From c6f1d2953870940f9c0dda83899c31a1ca10bcca Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 6 Oct 2021 15:28:13 -0700 Subject: [PATCH 01/19] Use PinnipedSession type instead of fosite's DefaultSesssion type This will allow us to store custom data inside the fosite session storage for all downstream OIDC sessions. Signed-off-by: Margo Crawford --- .../fositestorage/accesstoken/accesstoken.go | 6 +- .../accesstoken/accesstoken_test.go | 42 ++- .../authorizationcode/authorizationcode.go | 297 +++++++++--------- .../authorizationcode_test.go | 45 ++- internal/fositestorage/fositestorage.go | 6 +- .../openidconnect/openidconnect.go | 5 +- .../openidconnect/openidconnect_test.go | 29 +- internal/fositestorage/pkce/pkce.go | 6 +- internal/fositestorage/pkce/pkce_test.go | 30 +- .../refreshtoken/refreshtoken.go | 6 +- .../refreshtoken/refreshtoken_test.go | 42 ++- internal/oidc/auth/auth_handler.go | 15 +- .../downstreamsession/downstream_session.go | 17 +- internal/oidc/token/token_handler.go | 8 +- internal/oidc/token/token_handler_test.go | 55 ++-- internal/psession/pinniped_session.go | 99 ++++++ .../testutil/oidctestutil/oidctestutil.go | 26 +- internal/testutil/psession.go | 43 +++ 18 files changed, 462 insertions(+), 315 deletions(-) create mode 100644 internal/psession/pinniped_session.go create mode 100644 internal/testutil/psession.go diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index b4427543..fc2427ad 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -10,7 +10,6 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/openid" "k8s.io/apimachinery/pkg/api/errors" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -18,6 +17,7 @@ import ( "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( @@ -26,7 +26,7 @@ const ( ErrInvalidAccessTokenRequestVersion = constable.Error("access token request data has wrong version") ErrInvalidAccessTokenRequestData = constable.Error("access token request data must be present") - accessTokenStorageVersion = "1" + accessTokenStorageVersion = "2" ) type RevocationStorage interface { @@ -110,7 +110,7 @@ func newValidEmptyAccessTokenSession() *session { return &session{ Request: &fosite.Request{ Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, }, } } diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index 3c5b22df..dc468742 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -22,6 +21,8 @@ import ( coretesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" ) const namespace = "test-ns" @@ -51,7 +52,7 @@ func TestAccessTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -84,16 +85,10 @@ func TestAccessTokenStorage(t *testing.T) { RequestObjectSigningAlgorithm: "", TokenEndpointAuthSigningAlgorithm: "", }}, - RequestedScope: nil, - GrantedScope: nil, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Claims: nil, - Headers: nil, - ExpiresAt: nil, - Username: "snorlax", - Subject: "panda", - }, + RequestedScope: nil, + GrantedScope: nil, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), RequestedAudience: nil, GrantedAudience: nil, } @@ -107,6 +102,7 @@ func TestAccessTokenStorage(t *testing.T) { err = storage.DeleteAccessTokenSession(ctx, "fancy-signature") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -125,7 +121,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -151,11 +147,8 @@ func TestAccessTokenStorageRevocation(t *testing.T) { TokenEndpointAuthMethod: "something", }, }, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Username: "snorlax", - Subject: "panda", - }, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), } err := storage.CreateAccessTokenSession(ctx, "fancy-signature", request) require.NoError(t, err) @@ -164,6 +157,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) { err = storage.RevokeAccessToken(ctx, "abcd-1") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -190,7 +184,7 @@ func TestWrongVersion(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -200,7 +194,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAccessTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 1") + require.EqualError(t, err, "access token request data has wrong version: access token session for fancy-signature has version not-the-right-version instead of 2") } func TestNilSessionRequest(t *testing.T) { @@ -218,7 +212,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-token", @@ -246,10 +240,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { Client: &clientregistry.Client{}, } err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) - require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") + require.EqualError(t, err, "requester's session must be of type PinnipedSession") request = &fosite.Request{ - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: nil, } err = storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) @@ -261,7 +255,7 @@ func TestCreateWithoutRequesterID(t *testing.T) { request := &fosite.Request{ ID: "", // empty ID - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: &clientregistry.Client{}, } err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request) diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index f66c193c..a76f684f 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -11,7 +11,6 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/openid" "k8s.io/apimachinery/pkg/api/errors" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -19,6 +18,7 @@ import ( "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( @@ -27,7 +27,7 @@ const ( ErrInvalidAuthorizeRequestData = constable.Error("authorization request data must be present") ErrInvalidAuthorizeRequestVersion = constable.Error("authorization request data has wrong version") - authorizeCodeStorageVersion = "1" + authorizeCodeStorageVersion = "2" ) var _ oauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} @@ -139,7 +139,7 @@ func NewValidEmptyAuthorizeCodeSession() *AuthorizeCodeSession { return &AuthorizeCodeSession{ Request: &fosite.Request{ Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, }, } } @@ -169,161 +169,172 @@ func (e *errSerializationFailureWithCause) Error() string { // ExpectedAuthorizeCodeSessionJSONFromFuzzing is used for round tripping tests. // It is exported to allow integration tests to use it. const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ - "active": true, - "request": { + "active": true, + "request": { "id": "曑x螠Gæ鄋楨", "requestedAt": "2082-11-10T18:36:11.627253638Z", "client": { - "id": ":NJ¸Ɣ8(黋馛ÄRɴJa¶z", - "client_secret": "UQ==", - "redirect_uris": [ - "ǖ枭kʍ切厦ȳ箦;¥ʊXĝ奨誷傥祩d", - "zŇZ", - "優蒼ĊɌț訫DŽǽeʀO2ƚ&N" - ], - "grant_types": [ - "唐W6ɻ橩斚薛ɑƐ" - ], - "response_types": [ - "w", - "ǔŭe[u@阽羂ŷ-Ĵ½輢OÅ濲喾H" - ], - "scopes": [ - "G螩歐湡ƙı唡ɸğƎ&胢輢Ƈĵƚ" - ], - "audience": [ - "ě" - ], - "public": false, - "jwks_uri": "o*泞羅ʘ Ⱦķ瀊垰7ã\")", - "jwks": { - "keys": [ - { - "kty": "OKP", - "crv": "Ed25519", - "x": "nK9xgX_iN7u3u_i8YOO7ZRT_WK028Vd_nhtsUu7Eo6E", - "x5u": { - "Scheme": "", - "Opaque": "", - "User": null, - "Host": "", - "Path": "", - "RawPath": "", - "ForceQuery": false, - "RawQuery": "", - "Fragment": "", - "RawFragment": "" - } - }, - { - "kty": "OKP", - "crv": "Ed25519", - "x": "UbbswQgzWhfGCRlwQmMp6fw_HoIoqkIaKT-2XN2fuYU", - "x5u": { - "Scheme": "", - "Opaque": "", - "User": null, - "Host": "", - "Path": "", - "RawPath": "", - "ForceQuery": false, - "RawQuery": "", - "Fragment": "", - "RawFragment": "" - } - } - ] - }, - "token_endpoint_auth_method": "ƿʥǟȒ伉Q鱙翑ȲŻ", - "Issuer": "锰劝旣樎Ȱ鍌#ȳńƩŴȭ", - "Subject": "绝TFNJĆw宵ɚeY48珎²", - "Audience": [ - "éã越|j¦鲶H股ƲLŋZ-{5£踉4" - ], - "Nonce": "5^驜Ŗ~ů崧軒q腟u尿", - "ExpiresAt": "2065-11-30T13:47:03.613000626Z", - "IssuedAt": "1976-02-22T09:57:20.479850437Z", - "RequestedAt": "2016-04-13T04:18:53.648949323Z", - "AuthTime": "2098-07-12T04:38:54.034043015Z", - "AccessTokenHash": "嫯R", - "AuthenticationContextClassReference": "¤'+ʣ", - "AuthenticationMethodsReference": "L&ɽ艄ʬʏ", - "CodeHash": "ğǫ\\aȊ4ț髄Al", - "Extra": { - "PƢ曰": { - "ĸŴB岺Ð嫹Sx镯荫ő": [ - 843216989 - ], - "疂ư墫ɓ": { - "\\BRë_g\"ʎ啴SƇMǃļ": { - "ʦ4": false - }, - "鶡萷ɵ啜s攦": null - } - }, - "曓蓳n匟鯘磹*金爃鶴滱ůĮǐ_c3#": 2520197933 - } - }, - "Headers": { - "Extra": { - "寱ĊƑ÷Ƒ螞费Ďğ~劰û橸ɽ銐ƭ?}": { - "ȜʁɁ;Bd謺錳4帳ŅǃĊd": { - "翢砜Fȏl鐉诳DT=3骜": { - "ų厷ɁOƪ穋嶿鳈恱va|载ǰɱ汶C": false - }, - "鸨EJ毕懴řĬń戹%c": null + "fosite": { + "Claims": { + "JTI": "u妔隤ʑƍš駎竪0ɔ闏À1", + "Issuer": "麤ã桒嘞\\摗Ǘū稖咾鎅ǸÖ绝TF", + "Subject": "巽ēđų蓼tùZ蛆鬣a\"ÙǞ0觢Û±", + "Audience": [ + "H股ƲL", + "肟v\u0026đehpƧ", + "5^驜Ŗ~ů崧軒q腟u尿" + ], + "Nonce": "ğ", + "ExpiresAt": "2016-11-22T21:33:58.460521133Z", + "IssuedAt": "1990-07-25T23:42:07.055978334Z", + "RequestedAt": "1971-01-30T00:23:36.377684025Z", + "AuthTime": "2088-11-09T12:09:14.051840239Z", + "AccessTokenHash": "蕖¤'+ʣȍ瓁U4鞀", + "AuthenticationContextClassReference": "ʏÑęN\u003c_z", + "AuthenticationMethodsReference": "ț髄A", + "CodeHash": "4磔_袻vÓG-壧丵礴鋈k蟵pAɂʅ", + "Extra": { + "#\u0026PƢ曰l騌蘙螤\\阏Đ镴Ƥm蔻ǭ\\鿞": 1677215584, + "Y\u0026鶡萷ɵ啜s攦Ɩïdnǔ": { + ",t猟i\u0026\u0026Q@ǤǟǗǪ飘ȱF?Ƈ": { + "~劰û橸ɽ銐ƭ?}H": null, + "癑勦e骲v0H晦XŘO溪V蔓": { + "碼Ǫ": false + } + }, + "钻煐ɨəÅDČ{Ȩʦ4撎": [ + 3684968178 + ] + } + } }, - "室癑勦e骲v0H晦XŘO溪V蔓Ȍ+~ē": [ - 954647573 - ] - }, - "麈ƵDǀ\\郂üţ垂": 1572524915 + "Headers": { + "Extra": { + "ĊdŘ鸨EJ毕懴řĬń戹": { + "诳DT=3骜Ǹ,": { + "\u003e": { + "ǰ": false + }, + "ɁOƪ穋嶿鳈恱va": null + }, + "豑觳翢砜Fȏl": [ + 927958776 + ] + }, + "埅ȜʁɁ;Bd謺錳4帳Ņ": 388005986 + } + }, + "ExpiresAt": { + "C]ɲ'=ĸ闒NȢȰ.醋": "1970-07-19T18:03:29.902062193Z", + "fɤȆʪ融ƆuŤn": "2064-01-24T20:34:16.593152073Z", + "爣縗ɦüHêQ仏1ő": "2102-03-17T06:24:40.256846902Z" + }, + "Username": "韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ", + "Subject": "闺髉龳ǽÙ龦O亾EW莛8嘶×" + }, + "custom": { + "providerUID": "鵮碡ʯiŬŽ非Ĝ眧Ĭ葜SŦ餧Ĭ倏4", + "providerName": "nŐǛ3", + "providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt", + "oidc": { + "upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬" + } } - }, - "ExpiresAt": { - "'=ĸ闒NȢȰ.醋fʜ": "2031-10-18T22:07:34.950803105Z", - "ɦüHêQ仏1őƖ2Ė暮唍ǞʜƢú4": "2049-05-13T15:27:20.968432454Z" - }, - "Username": "+韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟȥE", - "Subject": "龳ǽÙ龦O亾EW莛8嘶×姮c恭企" }, "requestedAudience": [ - "邖ɐ5檄¬", - "Ĭ葜SŦ餧Ĭ倏4ĵ嶼仒篻ɥ闣ʬ橳(ý綃" + "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾" ], "grantedAudience": [ - "ʚƟ覣k眐4ĈtC嵽痊w©Ź榨Q|ô", - "猊Ia瓕巈環_ɑ彨ƍ蛊ʚ£:設虝2" + "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞" ] - }, - "version": "1" - }` + }, + "version": "2" +}` diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index b93481c9..fdc98e96 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -18,7 +18,6 @@ import ( fuzz "github.com/google/gofuzz" "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/openid" "github.com/pkg/errors" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" @@ -34,6 +33,8 @@ import ( "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" ) const namespace = "test-ns" @@ -62,7 +63,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -81,7 +82,7 @@ func TestAuthorizationCodeStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -113,16 +114,10 @@ func TestAuthorizationCodeStorage(t *testing.T) { TokenEndpointAuthSigningAlgorithm: "", }, }, - RequestedScope: nil, - GrantedScope: nil, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Claims: nil, - Headers: nil, - ExpiresAt: nil, - Username: "snorlax", - Subject: "panda", - }, + RequestedScope: nil, + GrantedScope: nil, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), RequestedAudience: nil, GrantedAudience: nil, } @@ -136,6 +131,8 @@ func TestAuthorizationCodeStorage(t *testing.T) { err = storage.InvalidateAuthorizeCodeSession(ctx, "fancy-signature") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed + testutil.LogActualJSONFromUpdateAction(t, client, 3) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) // Doing a Get on an invalidated session should still return the session, but also return an error. @@ -173,7 +170,7 @@ func TestInvalidateWhenConflictOnUpdateHappens(t *testing.T) { request := &fosite.Request{ ID: "some-request-id", Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: testutil.NewFakePinnipedSession(), } err := storage.CreateAuthorizeCodeSession(ctx, "fancy-signature", request) require.NoError(t, err) @@ -193,7 +190,7 @@ func TestWrongVersion(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version", "active": true}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version","active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -203,7 +200,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetAuthorizeCodeSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 1") + require.EqualError(t, err, "authorization request data has wrong version: authorization code session for fancy-signature has version not-the-right-version instead of 2") } func TestNilSessionRequest(t *testing.T) { @@ -218,7 +215,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"1", "active": true}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value", "version":"2", "active": true}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/authcode", @@ -246,10 +243,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { Client: &clientregistry.Client{}, } err := storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request) - require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") + require.EqualError(t, err, "requester's session must be of type PinnipedSession") request = &fosite.Request{ - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: nil, } err = storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request) @@ -274,7 +271,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // checked above defaultClient := validSession.Request.Client.(*clientregistry.Client) - defaultSession := validSession.Request.Session.(*openid.DefaultSession) + pinnipedSession := validSession.Request.Session.(*psession.PinnipedSession) // makes it easier to use a raw string replacer := strings.NewReplacer("`", "a") @@ -297,12 +294,12 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { *fc = defaultClient }, func(fs *fosite.Session, c fuzz.Continue) { - c.Fuzz(defaultSession) - *fs = defaultSession + c.Fuzz(pinnipedSession) + *fs = pinnipedSession }, // these types contain an interface{} that we need to handle - // this is safe because we explicitly provide the openid.DefaultSession concrete type + // this is safe because we explicitly provide the PinnipedSession concrete type func(value *map[string]interface{}, c fuzz.Continue) { // cover all the JSON data types just in case *value = map[string]interface{}{ @@ -382,7 +379,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) { // set these to match CreateAuthorizeCodeSession so that .JSONEq works validSession.Active = true - validSession.Version = "1" + validSession.Version = "2" validSessionJSONBytes, err := json.MarshalIndent(validSession, "", "\t") require.NoError(t, err) diff --git a/internal/fositestorage/fositestorage.go b/internal/fositestorage/fositestorage.go index bf1c20e9..d3c8f476 100644 --- a/internal/fositestorage/fositestorage.go +++ b/internal/fositestorage/fositestorage.go @@ -5,16 +5,16 @@ package fositestorage import ( "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request") ErrInvalidClientType = constable.Error("requester's client must be of type clientregistry.Client") - ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession") + ErrInvalidSessionType = constable.Error("requester's session must be of type PinnipedSession") StorageRequestIDLabelName = "storage.pinniped.dev/request-id" //nolint:gosec // this is not a credential ) @@ -27,7 +27,7 @@ func ValidateAndExtractAuthorizeRequest(requester fosite.Requester) (*fosite.Req if !ok2 { return nil, ErrInvalidClientType } - _, ok3 := request.Session.(*openid.DefaultSession) + _, ok3 := request.Session.(*psession.PinnipedSession) if !ok3 { return nil, ErrInvalidSessionType } diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index f747a667..f1e2200f 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -18,6 +18,7 @@ import ( "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( @@ -27,7 +28,7 @@ const ( ErrInvalidOIDCRequestData = constable.Error("oidc request data must be present") ErrMalformedAuthorizationCode = constable.Error("malformed authorization code") - oidcStorageVersion = "1" + oidcStorageVersion = "2" ) var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} @@ -112,7 +113,7 @@ func newValidEmptyOIDCSession() *session { return &session{ Request: &fosite.Request{ Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, }, } } diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 6328ffa2..ba76fe8e 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -22,6 +22,8 @@ import ( coretesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" ) const namespace = "test-ns" @@ -50,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -84,16 +86,10 @@ func TestOpenIdConnectStorage(t *testing.T) { TokenEndpointAuthSigningAlgorithm: "", }, }, - RequestedScope: nil, - GrantedScope: nil, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Claims: nil, - Headers: nil, - ExpiresAt: nil, - Username: "snorlax", - Subject: "panda", - }, + RequestedScope: nil, + GrantedScope: nil, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), RequestedAudience: nil, GrantedAudience: nil, } @@ -107,6 +103,7 @@ func TestOpenIdConnectStorage(t *testing.T) { err = storage.DeleteOpenIDConnectSession(ctx, "fancy-code.fancy-signature") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -130,7 +127,7 @@ func TestWrongVersion(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -140,7 +137,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetOpenIDConnectSession(ctx, "fancy-code.fancy-signature", nil) - require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 1") + require.EqualError(t, err, "oidc request data has wrong version: oidc session for fancy-signature has version not-the-right-version instead of 2") } func TestNilSessionRequest(t *testing.T) { @@ -155,7 +152,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/oidc", @@ -183,10 +180,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { Client: &clientregistry.Client{}, } err := storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request) - require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") + require.EqualError(t, err, "requester's session must be of type PinnipedSession") request = &fosite.Request{ - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: nil, } err = storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request) diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index 424a7855..cf6cec69 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -9,7 +9,6 @@ import ( "time" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" "k8s.io/apimachinery/pkg/api/errors" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -18,6 +17,7 @@ import ( "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( @@ -26,7 +26,7 @@ const ( ErrInvalidPKCERequestVersion = constable.Error("pkce request data has wrong version") ErrInvalidPKCERequestData = constable.Error("pkce request data must be present") - pkceStorageVersion = "1" + pkceStorageVersion = "2" ) var _ pkce.PKCERequestStorage = &pkceStorage{} @@ -96,7 +96,7 @@ func newValidEmptyPKCESession() *session { return &session{ Request: &fosite.Request{ Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, }, } } diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index 671797eb..d57649be 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -23,6 +22,8 @@ import ( coretesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" ) const namespace = "test-ns" @@ -51,7 +52,7 @@ func TestPKCEStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -85,16 +86,10 @@ func TestPKCEStorage(t *testing.T) { TokenEndpointAuthSigningAlgorithm: "", }, }, - RequestedScope: nil, - GrantedScope: nil, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Claims: nil, - Headers: nil, - ExpiresAt: nil, - Username: "snorlax", - Subject: "panda", - }, + RequestedScope: nil, + GrantedScope: nil, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), RequestedAudience: nil, GrantedAudience: nil, } @@ -108,6 +103,7 @@ func TestPKCEStorage(t *testing.T) { err = storage.DeletePKCERequestSession(ctx, "fancy-signature") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -134,7 +130,7 @@ func TestWrongVersion(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -144,7 +140,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetPKCERequestSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 1") + require.EqualError(t, err, "pkce request data has wrong version: pkce session for fancy-signature has version not-the-right-version instead of 2") } func TestNilSessionRequest(t *testing.T) { @@ -162,7 +158,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pkce", @@ -190,10 +186,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { Client: &clientregistry.Client{}, } err := storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request) - require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") + require.EqualError(t, err, "requester's session must be of type PinnipedSession") request = &fosite.Request{ - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: nil, } err = storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request) diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index f53d9bf7..a456ed75 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -10,7 +10,6 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/oauth2" - "github.com/ory/fosite/handler/openid" "k8s.io/apimachinery/pkg/api/errors" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" @@ -18,6 +17,7 @@ import ( "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/fositestorage" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" ) const ( @@ -26,7 +26,7 @@ const ( ErrInvalidRefreshTokenRequestVersion = constable.Error("refresh token request data has wrong version") ErrInvalidRefreshTokenRequestData = constable.Error("refresh token request data must be present") - refreshTokenStorageVersion = "1" + refreshTokenStorageVersion = "2" ) type RevocationStorage interface { @@ -110,7 +110,7 @@ func newValidEmptyRefreshTokenSession() *session { return &session{ Request: &fosite.Request{ Client: &clientregistry.Client{}, - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, }, } } diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index 10ee75bf..e8a155c9 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/pkg/errors" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -22,6 +21,8 @@ import ( coretesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/oidc/clientregistry" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" ) const namespace = "test-ns" @@ -50,7 +51,7 @@ func TestRefreshTokenStorage(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -84,16 +85,10 @@ func TestRefreshTokenStorage(t *testing.T) { TokenEndpointAuthSigningAlgorithm: "", }, }, - RequestedScope: nil, - GrantedScope: nil, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Claims: nil, - Headers: nil, - ExpiresAt: nil, - Username: "snorlax", - Subject: "panda", - }, + RequestedScope: nil, + GrantedScope: nil, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), RequestedAudience: nil, GrantedAudience: nil, } @@ -107,6 +102,7 @@ func TestRefreshTokenStorage(t *testing.T) { err = storage.DeleteRefreshTokenSession(ctx, "fancy-signature") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -125,7 +121,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"1"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -151,11 +147,8 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { TokenEndpointAuthMethod: "something", }, }, - Form: url.Values{"key": []string{"val"}}, - Session: &openid.DefaultSession{ - Username: "snorlax", - Subject: "panda", - }, + Form: url.Values{"key": []string{"val"}}, + Session: testutil.NewFakePinnipedSession(), } err := storage.CreateRefreshTokenSession(ctx, "fancy-signature", request) require.NoError(t, err) @@ -164,6 +157,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) { err = storage.RevokeRefreshToken(ctx, "abcd-1") require.NoError(t, err) + testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed require.Equal(t, wantActions, client.Actions()) } @@ -190,7 +184,7 @@ func TestWrongVersion(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"requestedAudience":null,"grantedAudience":null},"version":"not-the-right-version"}`), + "pinniped-storage-data": []byte(`{"request":{"id":"abcd-1"},"version":"not-the-right-version"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -200,7 +194,7 @@ func TestWrongVersion(t *testing.T) { _, err = storage.GetRefreshTokenSession(ctx, "fancy-signature", nil) - require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 1") + require.EqualError(t, err, "refresh token request data has wrong version: refresh token session for fancy-signature has version not-the-right-version instead of 2") } func TestNilSessionRequest(t *testing.T) { @@ -218,7 +212,7 @@ func TestNilSessionRequest(t *testing.T) { }, }, Data: map[string][]byte{ - "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"1"}`), + "pinniped-storage-data": []byte(`{"nonsense-key": "nonsense-value","version":"2"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/refresh-token", @@ -246,10 +240,10 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) { Client: &clientregistry.Client{}, } err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) - require.EqualError(t, err, "requester's session must be of type openid.DefaultSession") + require.EqualError(t, err, "requester's session must be of type PinnipedSession") request = &fosite.Request{ - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: nil, } err = storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) @@ -261,7 +255,7 @@ func TestCreateWithoutRequesterID(t *testing.T) { request := &fosite.Request{ ID: "", // empty ID - Session: &openid.DefaultSession{}, + Session: &psession.PinnipedSession{}, Client: &clientregistry.Client{}, } err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 5dd9e211..ed6e1137 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -25,6 +25,7 @@ import ( "go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -173,12 +174,14 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( } now := time.Now() - _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. - Subject: "none", - AuthTime: now, - RequestedAt: now, + _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. + Subject: "none", + AuthTime: now, + RequestedAt: now, + }, }, }) if err != nil { diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/oidc/downstreamsession/downstream_session.go index 64838d5e..bd29646e 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/oidc/downstreamsession/downstream_session.go @@ -18,6 +18,7 @@ import ( "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" ) const ( @@ -35,19 +36,21 @@ const ( ) // MakeDownstreamSession creates a downstream OIDC session. -func MakeDownstreamSession(subject string, username string, groups []string) *openid.DefaultSession { +func MakeDownstreamSession(subject string, username string, groups []string) *psession.PinnipedSession { now := time.Now().UTC() - openIDSession := &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Subject: subject, - RequestedAt: now, - AuthTime: now, + openIDSession := &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: subject, + RequestedAt: now, + AuthTime: now, + }, }, } if groups == nil { groups = []string{} } - openIDSession.Claims.Extra = map[string]interface{}{ + openIDSession.IDTokenClaims().Extra = map[string]interface{}{ oidc.DownstreamUsernameClaim: username, oidc.DownstreamGroupsClaim: groups, } diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index c481168b..72222367 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package token provides a handler for the OIDC token endpoint. @@ -8,19 +8,19 @@ import ( "net/http" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" ) func NewHandler( oauthHelper fosite.OAuth2Provider, ) http.Handler { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - var session openid.DefaultSession - accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, &session) + session := psession.NewPinnipedSession() + accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, session) if err != nil { plog.Info("token request error", oidc.FositeErrorForLog(err)...) oauthHelper.WriteAccessError(w, accessRequest, err) diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 9032dd10..c8a36153 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -46,6 +46,7 @@ import ( "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" ) @@ -58,7 +59,6 @@ const ( goodNonce = "some-nonce-value-with-enough-bytes-to-exceed-min-allowed" goodSubject = "https://issuer?sub=some-subject" goodUsername = "some-username" - goodGroups = "group1,groups2" hmacSecret = "this needs to be at least 32 characters to meet entropy requirements" @@ -72,6 +72,8 @@ const ( var ( goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC) goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.UTC) + goodGroups = []string{"group1", "groups2"} + expectedGoodGroups = []interface{}{"group1", "groups2"} hmacSecretFunc = func() []byte { return []byte(hmacSecret) @@ -813,7 +815,7 @@ func TestTokenExchange(t *testing.T) { require.Equal(t, goodSubject, tokenClaims["sub"]) require.Equal(t, goodIssuer, tokenClaims["iss"]) require.Equal(t, goodUsername, tokenClaims["username"]) - require.Equal(t, goodGroups, tokenClaims["groups"]) + require.Equal(t, expectedGoodGroups, tokenClaims["groups"]) // Also assert that some are the same as the original downstream ID token. requireClaimsAreEqual(t, "iss", claimsOfFirstIDToken, tokenClaims) // issuer @@ -1358,18 +1360,25 @@ func makeOauthHelperWithNilPrivateJWTSigningKey( func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider) fosite.AuthorizeResponder { // We only set the fields in the session that Fosite wants us to set. ctx := context.Background() - session := &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - Subject: goodSubject, - RequestedAt: goodRequestedAtTime, - AuthTime: goodAuthTime, - Extra: map[string]interface{}{ - oidc.DownstreamUsernameClaim: goodUsername, - oidc.DownstreamGroupsClaim: goodGroups, + session := &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: goodSubject, + RequestedAt: goodRequestedAtTime, + AuthTime: goodAuthTime, + Extra: map[string]interface{}{ + oidc.DownstreamUsernameClaim: goodUsername, + oidc.DownstreamGroupsClaim: goodGroups, + }, + }, + Subject: "", // 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{ + OIDC: &psession.OIDCSessionData{ + UpstreamRefreshToken: "starting-fake-refresh-token", }, }, - Subject: "", // not used, note that callback_handler.go does not set this - Username: "", // not used, note that callback_handler.go does not set this } authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) require.NoError(t, err) @@ -1588,19 +1597,19 @@ func requireValidStoredRequest( require.Equal(t, wantRequestForm, request.GetRequestForm()) // Fosite stores access token request without form // Cast session to the type we think it should be. - session, ok := request.GetSession().(*openid.DefaultSession) - require.Truef(t, ok, "could not cast %T to %T", request.GetSession(), &openid.DefaultSession{}) + session, ok := request.GetSession().(*psession.PinnipedSession) + require.Truef(t, ok, "could not cast %T to %T", request.GetSession(), &psession.PinnipedSession{}) // Assert that the session claims are what we think they should be, but only if we are doing OIDC. if contains(wantGrantedScopes, "openid") { - claims := session.Claims + claims := session.Fosite.Claims require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field. require.Equal(t, goodSubject, claims.Subject) // Our custom claims from the authorize endpoint should still be set. require.Equal(t, map[string]interface{}{ "username": goodUsername, - "groups": goodGroups, + "groups": expectedGoodGroups, }, claims.Extra) // We are in charge of setting these fields. For the purpose of testing, we ensure that the @@ -1610,7 +1619,7 @@ func requireValidStoredRequest( // These fields will all be given good defaults by fosite at runtime and we only need to use them // if we want to override the default behaviors. We currently don't need to override these defaults, - // so they do not end up being stored. Fosite sets its defaults at runtime in openid.DefaultSession's + // so they do not end up being stored. Fosite sets its defaults at runtime in openid.DefaultStrategy's // GenerateIDToken() method. require.Empty(t, claims.Issuer) require.Empty(t, claims.Audience) @@ -1630,11 +1639,11 @@ func requireValidStoredRequest( } // Assert that the session headers are what we think they should be. - headers := session.Headers + headers := session.Fosite.Headers require.Empty(t, headers) // Assert that the token expirations are what we think they should be. - authCodeExpiresAt, ok := session.ExpiresAt[fosite.AuthorizeCode] + authCodeExpiresAt, ok := session.Fosite.ExpiresAt[fosite.AuthorizeCode] require.True(t, ok, "expected session to hold expiration time for auth code") testutil.RequireTimeInDelta( t, @@ -1644,7 +1653,7 @@ func requireValidStoredRequest( ) // OpenID Connect sessions do not store access token expiration information. - accessTokenExpiresAt, ok := session.ExpiresAt[fosite.AccessToken] + accessTokenExpiresAt, ok := session.Fosite.ExpiresAt[fosite.AccessToken] if wantAccessTokenExpiresAt { require.True(t, ok, "expected session to hold expiration time for access token") testutil.RequireTimeInDelta( @@ -1658,8 +1667,8 @@ func requireValidStoredRequest( } // We don't use these, so they should be empty. - require.Empty(t, session.Username) - require.Empty(t, session.Subject) + require.Empty(t, session.Fosite.Username) + require.Empty(t, session.Fosite.Subject) } func requireGarbageCollectTimeInDelta(t *testing.T, tokenString string, typeLabel string, secrets v1.SecretInterface, wantExpirationTime time.Time, deltaTime time.Duration) { @@ -1709,7 +1718,7 @@ func requireValidIDToken( IssuedAt int64 `json:"iat"` RequestedAt int64 `json:"rat"` AuthTime int64 `json:"auth_time"` - Groups string `json:"groups"` + Groups []string `json:"groups"` Username string `json:"username"` } diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go new file mode 100644 index 00000000..334dce06 --- /dev/null +++ b/internal/psession/pinniped_session.go @@ -0,0 +1,99 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package psession + +import ( + "time" + + "github.com/mohae/deepcopy" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/token/jwt" +) + +// PinnipedSession is a session container which includes the fosite standard stuff plus custom Pinniped stuff. +type PinnipedSession struct { + // Delegate most things to the standard fosite OpenID JWT session. + Fosite *openid.DefaultSession `json:"fosite,omitempty"` + + // Custom Pinniped extensions to the session data. + Custom *PinnipedSessionData `json:"custom,omitempty"` +} + +var _ openid.Session = &PinnipedSession{} + +// PinnipedSessionData 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. +type PinnipedSessionData struct { + // 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 + // 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 + // using that identity provider will not be able to perform any more downstream refreshes. + ProviderUID string `json:"providerUID"` + + // 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. + // Also used to decide which of the pointer types below should be used. + ProviderName string `json:"providerName"` + + // 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. + ProviderType string `json:"providerType"` + + // Only used when ProviderType == "oidc". + OIDC *OIDCSessionData `json:"oidc,omitempty"` +} + +// OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider. +type OIDCSessionData struct { + UpstreamRefreshToken string `json:"upstreamRefreshToken"` +} + +// NewPinnipedSession returns a new empty session. +func NewPinnipedSession() *PinnipedSession { + return &PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{}, + Headers: &jwt.Headers{}, + }, + Custom: &PinnipedSessionData{}, + } +} + +func (s *PinnipedSession) Clone() fosite.Session { + // Implementation copied from openid.DefaultSession's clone method. + if s == nil { + return nil + } + return deepcopy.Copy(s).(fosite.Session) +} + +func (s *PinnipedSession) SetExpiresAt(key fosite.TokenType, exp time.Time) { + s.Fosite.SetExpiresAt(key, exp) +} + +func (s *PinnipedSession) GetExpiresAt(key fosite.TokenType) time.Time { + return s.Fosite.GetExpiresAt(key) +} + +func (s *PinnipedSession) GetUsername() string { + return s.Fosite.GetUsername() +} + +func (s *PinnipedSession) SetSubject(subject string) { + s.Fosite.SetSubject(subject) +} + +func (s *PinnipedSession) GetSubject() string { + return s.Fosite.GetSubject() +} + +func (s *PinnipedSession) IDTokenHeaders() *jwt.Headers { + return s.Fosite.IDTokenHeaders() +} + +func (s *PinnipedSession) IDTokenClaims() *jwt.IDTokenClaims { + return s.Fosite.IDTokenClaims() +} diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 5666e610..5ba9512a 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -16,7 +16,6 @@ import ( coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" @@ -31,6 +30,7 @@ import ( pkce2 "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -563,7 +563,7 @@ func validateAuthcodeStorage( wantDownstreamRequestedScopes []string, wantDownstreamClientID string, wantDownstreamRedirectURI string, -) (*fosite.Request, *openid.DefaultSession) { +) (*fosite.Request, *psession.PinnipedSession) { t.Helper() const ( @@ -591,16 +591,16 @@ func validateAuthcodeStorage( testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) // We're not using these fields yet, so confirm that we did not set them (for now). - require.Empty(t, storedSessionFromAuthcode.Subject) - require.Empty(t, storedSessionFromAuthcode.Username) - require.Empty(t, storedSessionFromAuthcode.Headers) + require.Empty(t, storedSessionFromAuthcode.Fosite.Subject) + require.Empty(t, storedSessionFromAuthcode.Fosite.Username) + require.Empty(t, storedSessionFromAuthcode.Fosite.Headers) // The authcode that we are issuing should be good for the length of time that we declare in the fosite config. - testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) - require.Len(t, storedSessionFromAuthcode.ExpiresAt, 1) + testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.Fosite.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) + require.Len(t, storedSessionFromAuthcode.Fosite.ExpiresAt, 1) // Now confirm the ID token claims. - actualClaims := storedSessionFromAuthcode.Claims + actualClaims := storedSessionFromAuthcode.Fosite.Claims // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) @@ -642,7 +642,7 @@ func validatePKCEStorage( oauthStore fositestoragei.AllFositeStorage, storeKey string, storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *openid.DefaultSession, + storedSessionFromAuthcode *psession.PinnipedSession, wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, ) { t.Helper() @@ -667,7 +667,7 @@ func validateIDSessionStorage( oauthStore fositestoragei.AllFositeStorage, storeKey string, storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *openid.DefaultSession, + storedSessionFromAuthcode *psession.PinnipedSession, wantDownstreamNonce string, ) { t.Helper() @@ -686,13 +686,13 @@ func validateIDSessionStorage( require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) } -func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *openid.DefaultSession) { +func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *psession.PinnipedSession) { t.Helper() storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) - storedSession, ok := storedAuthorizeRequest.GetSession().(*openid.DefaultSession) - require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &openid.DefaultSession{}) + storedSession, ok := storedAuthorizeRequest.GetSession().(*psession.PinnipedSession) + require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &psession.PinnipedSession{}) return storedRequest, storedSession } diff --git a/internal/testutil/psession.go b/internal/testutil/psession.go new file mode 100644 index 00000000..d6f4bcc8 --- /dev/null +++ b/internal/testutil/psession.go @@ -0,0 +1,43 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package testutil + +import ( + "testing" + + "github.com/ory/fosite/handler/openid" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/fake" + testing2 "k8s.io/client-go/testing" + + "go.pinniped.dev/internal/psession" +) + +func NewFakePinnipedSession() *psession.PinnipedSession { + return &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: nil, + Headers: nil, + ExpiresAt: nil, + Username: "snorlax", + Subject: "panda", + }, + Custom: &psession.PinnipedSessionData{ + ProviderUID: "fake-provider-uid", + ProviderType: "fake-provider-type", + ProviderName: "fake-provider-name", + OIDC: &psession.OIDCSessionData{ + UpstreamRefreshToken: "fake-upstream-refresh-token", + }, + }, + } +} + +func LogActualJSONFromCreateAction(t *testing.T, client *fake.Clientset, actionIndex int) { + t.Log("actual value of CreateAction secret data", string(client.Actions()[actionIndex].(testing2.CreateActionImpl).Object.(*v1.Secret).Data["pinniped-storage-data"])) +} + +func LogActualJSONFromUpdateAction(t *testing.T, client *fake.Clientset, actionIndex int) { + t.Log("actual value of UpdateAction secret data", string(client.Actions()[actionIndex].(testing2.UpdateActionImpl).Object.(*v1.Secret).Data["pinniped-storage-data"])) +} From 43244b6599e65027115e922c8e12b2def1590e54 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 6 Oct 2021 16:30:30 -0700 Subject: [PATCH 02/19] Do not pass through downstream prompt param - throw an error when prompt=none because the spec says we can't ignore it - ignore the other prompt params Signed-off-by: Ryan Richard --- internal/oidc/auth/auth_handler.go | 24 ++++++++++++---------- internal/oidc/auth/auth_handler_test.go | 27 +++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index ed6e1137..5ff4c1a9 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -30,6 +30,8 @@ import ( "go.pinniped.dev/pkg/oidcclient/pkce" ) +const promptParamNone = "none" + func NewHandler( downstreamIssuer string, idpLister oidc.UpstreamIdentityProvidersLister, @@ -220,15 +222,6 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( return err } - if csrfFromCookie == "" { - // We did not receive an incoming CSRF cookie, so write a new one. - err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) - if err != nil { - plog.Error("error setting CSRF cookie", err) - return err - } - } - authCodeOptions := []oauth2.AuthCodeOption{ oauth2.AccessTypeOffline, nonceValue.Param(), @@ -237,8 +230,17 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( } promptParam := r.Form.Get("prompt") - if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { - authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam)) + if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { + return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired) + } + + if csrfFromCookie == "" { + // We did not receive an incoming CSRF cookie, so write a new one. + err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) + if err != nil { + plog.Error("error setting CSRF cookie", err) + return err + } } http.Redirect(w, r, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index c0198ee1..8762f9aa 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -181,6 +181,12 @@ func TestAuthorizationEndpoint(t *testing.T) { "error_description": "The resource owner or authorization server denied the request. Reason: required claim in upstream ID token has invalid format.", "state": happyState, } + + fositeLoginRequiredErrorQuery = map[string]string{ + "error": "login_required", + "error_description": "The Authorization Server requires End-User authentication.", + "state": happyState, + } ) hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } @@ -630,7 +636,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, { - name: "OIDC upstream browser flow happy path with prompt param login passed through to redirect uri", + name: "OIDC upstream browser flow happy path with prompt param other than none that gets ignored", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -645,9 +651,26 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), "login"), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, }, + { + 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()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}), + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery), + wantBodyString: "", + }, { 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()), From 1bd346cbeb7826654edda57f7e0b88d10044cde0 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 8 Oct 2021 15:48:21 -0700 Subject: [PATCH 03/19] 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 --- .../active_directory_upstream_watcher.go | 5 +- .../active_directory_upstream_watcher_test.go | 82 +++-- .../ldap_upstream_watcher.go | 5 +- .../ldap_upstream_watcher_test.go | 52 ++-- .../oidc_upstream_watcher.go | 9 +- .../oidc_upstream_watcher_test.go | 87 +++--- .../authorizationcode_test.go | 4 + .../mockupstreamoidcidentityprovider.go | 29 ++ internal/oidc/auth/auth_handler.go | 61 +++- internal/oidc/auth/auth_handler_test.go | 292 ++++++++++++------ internal/oidc/callback/callback_handler.go | 18 +- .../oidc/callback/callback_handler_test.go | 58 +++- .../downstreamsession/downstream_session.go | 3 +- .../provider/dynamic_upstream_idp_provider.go | 10 + .../oidc/provider/manager/manager_test.go | 1 + internal/oidc/token/token_handler_test.go | 2 +- internal/psession/pinniped_session.go | 21 +- .../testutil/oidctestutil/oidctestutil.go | 106 +++++-- internal/testutil/psession.go | 2 +- internal/upstreamldap/upstreamldap.go | 8 + internal/upstreamoidc/upstreamoidc.go | 25 +- 21 files changed, 636 insertions(+), 244 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index d6490846..7bd83f39 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -301,8 +301,9 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, adUpstreamImpl := &activeDirectoryUpstreamGenericLDAPImpl{activeDirectoryIdentityProvider: *upstream} config := &upstreamldap.ProviderConfig{ - Name: upstream.Name, - Host: spec.Host, + Name: upstream.Name, + ResourceUID: upstream.UID, + Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, Filter: adUpstreamImpl.Spec().UserSearch().Filter(), diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 33aea395..da204216 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -150,6 +150,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { const ( testNamespace = "test-namespace" + testResourceUID = "test-uid" testName = "test-name" testSecretName = "test-bind-secret" testBindUsername = "test-bind-username" @@ -172,7 +173,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) 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{ Host: testHost, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, @@ -202,6 +203,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -364,7 +366,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -379,7 +381,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -407,7 +409,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -434,7 +436,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -460,7 +462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -486,7 +488,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -517,6 +519,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: nil, @@ -537,7 +540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -572,6 +575,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: nil, @@ -592,7 +596,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -630,6 +634,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: "ldap.example.com", ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS CABundle: testCABundle, @@ -650,7 +655,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", 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 { Name: testName, + ResourceUID: testResourceUID, 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 CABundle: testCABundle, @@ -709,7 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -745,6 +751,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: nil, @@ -765,7 +772,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -779,6 +786,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Name = "other-upstream" upstream.Generation = 42 upstream.Spec.Bind.SecretName = "non-existent-secret" + upstream.UID = "other-uid" })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { @@ -790,7 +798,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Error", 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -831,7 +839,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -872,6 +880,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -892,7 +901,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -928,7 +937,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -966,7 +975,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -995,6 +1004,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1015,7 +1025,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -1045,6 +1055,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1065,7 +1076,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -1100,7 +1111,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -1137,7 +1148,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -1175,7 +1186,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -1212,7 +1223,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -1243,6 +1254,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1264,7 +1276,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -1295,6 +1307,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1315,7 +1328,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -1350,6 +1363,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1370,7 +1384,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -1399,6 +1413,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1419,7 +1434,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -1447,7 +1462,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -1483,7 +1498,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -1525,7 +1540,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -1554,7 +1569,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -1594,6 +1609,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -1614,7 +1630,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index af748da1..bcbd6d91 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -226,8 +226,9 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * spec := upstream.Spec config := &upstreamldap.ProviderConfig{ - Name: upstream.Name, - Host: spec.Host, + Name: upstream.Name, + ResourceUID: upstream.UID, + Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index e9ab4813..c562d969 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -150,6 +150,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { const ( testNamespace = "test-namespace" testName = "test-name" + testResourceUID = "test-resource-uid" testSecretName = "test-bind-secret" testBindUsername = "test-bind-username" testBindPassword = "test-bind-password" @@ -171,7 +172,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) 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{ Host: testHost, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, @@ -201,6 +207,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: testCABundle, @@ -299,7 +306,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -320,7 +327,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -348,7 +355,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -375,7 +382,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -401,7 +408,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -427,7 +434,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -458,6 +465,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: nil, @@ -477,7 +485,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -519,6 +527,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: "ldap.example.com", ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS CABundle: testCABundle, @@ -538,7 +547,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", 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 { Name: testName, + ResourceUID: testResourceUID, 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 CABundle: testCABundle, @@ -600,7 +610,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -635,6 +645,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, + ResourceUID: testResourceUID, Host: testHost, ConnectionProtocol: upstreamldap.TLS, CABundle: nil, @@ -654,7 +665,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -674,6 +685,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { upstream.Name = "other-upstream" upstream.Generation = 42 upstream.Spec.Bind.SecretName = "non-existent-secret" + upstream.UID = "other-uid" })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { @@ -685,7 +697,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Error", 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -729,7 +741,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Error", Conditions: []v1alpha1.Condition{ @@ -771,7 +783,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -805,7 +817,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithStartTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -841,7 +853,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -877,7 +889,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -917,7 +929,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), @@ -954,7 +966,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, 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{ Phase: "Ready", Conditions: allConditionsTrue(1234, "4242"), diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index f7b52325..31293951 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -172,9 +172,11 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst Config: &oauth2.Config{ Scopes: computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), }, - UsernameClaim: upstream.Spec.Claims.Username, - GroupsClaim: upstream.Spec.Claims.Groups, - AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant, + UsernameClaim: upstream.Spec.Claims.Username, + GroupsClaim: upstream.Spec.Claims.Groups, + AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant, + AdditionalAuthcodeParams: map[string]string{"prompt": "consent"}, + ResourceUID: upstream.UID, } conditions := []*v1alpha1.Condition{ c.validateSecret(upstream, &result), @@ -374,6 +376,7 @@ func computeScopes(additionalScopes []string) []string { // First compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) set["openid"] = true + set["offline_access"] = true for _, s := range additionalScopes { set[s] = true } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index fd1a864d..73b0d2eb 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -18,6 +18,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" @@ -119,16 +120,18 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wrongCABase64 := base64.StdEncoding.EncodeToString(wrongCA.Bundle()) var ( - testNamespace = "test-namespace" - testName = "test-name" - testSecretName = "test-client-secret" - testAdditionalScopes = []string{"scope1", "scope2", "scope3"} - testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"} - testClientID = "test-oidc-client-id" - testClientSecret = "test-oidc-client-secret" - testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)} - testGroupsClaim = "test-groups-claim" - testUsernameClaim = "test-username-claim" + testNamespace = "test-namespace" + testName = "test-name" + testSecretName = "test-client-secret" + testAdditionalScopes = []string{"scope1", "scope2", "scope3"} + testExpectedScopes = []string{"offline_access", "openid", "scope1", "scope2", "scope3"} + testExpectedAdditionalParams = map[string]string{"prompt": "consent"} + testClientID = "test-oidc-client-id" + testClientSecret = "test-oidc-client-secret" + testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)} + testGroupsClaim = "test-groups-claim" + testUsernameClaim = "test-username-claim" + testUID = types.UID("test-uid") ) tests := []struct { name string @@ -561,13 +564,13 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana { name: "upstream with error becomes valid", 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{ Issuer: testIssuerURL, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"), + AdditionalScopes: append(testAdditionalScopes, "xyz", "openid", "offline_access"), AllowPasswordGrant: true, }, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, @@ -591,17 +594,19 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: testName, - ClientID: testClientID, - AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: append(testExpectedScopes, "xyz"), - UsernameClaim: testUsernameClaim, - GroupsClaim: testGroupsClaim, - AllowPasswordGrant: true, + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + Scopes: append(testExpectedScopes, "xyz"), + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: true, + AdditionalAuthcodeParams: testExpectedAdditionalParams, + ResourceUID: testUID, }, }, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testUID}, Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -614,7 +619,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana { name: "existing valid upstream", 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{ Issuer: testIssuerURL, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, @@ -644,17 +649,19 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: testName, - ClientID: testClientID, - AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: testExpectedScopes, - UsernameClaim: testUsernameClaim, - GroupsClaim: testGroupsClaim, - AllowPasswordGrant: false, + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + Scopes: testExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: testExpectedAdditionalParams, + ResourceUID: testUID, }, }, 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{ Phase: "Ready", Conditions: []v1alpha1.Condition{ @@ -667,7 +674,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana { name: "existing valid upstream with trailing slash", 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{ Issuer: testIssuerURL + "/ends-with-slash/", TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, @@ -694,17 +701,19 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: testName, - ClientID: testClientID, - AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: testExpectedScopes, - UsernameClaim: testUsernameClaim, - GroupsClaim: testGroupsClaim, - AllowPasswordGrant: false, + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + Scopes: testExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: testExpectedAdditionalParams, + ResourceUID: testUID, }, }, 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{ Phase: "Ready", 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].GetGroupsClaim(), actualIDP.GetGroupsClaim()) 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()) // We always want to use the proxy from env on these clients, so although the following assertions diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index fdc98e96..50ac0c28 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/clock" "k8s.io/client-go/kubernetes/fake" 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) { *s = fosite.TokenType(randString(c)) }, + func(s *types.UID, c fuzz.Continue) { + *s = types.UID(randString(c)) + }, // handle string type alias func(s *fosite.Arguments, c fuzz.Continue) { n := c.Intn(3) + 1 // 1 to 3 items diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index 152f33e2..dfbe785b 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -18,6 +18,7 @@ import ( oidctypes "go.pinniped.dev/pkg/oidcclient/oidctypes" pkce "go.pinniped.dev/pkg/oidcclient/pkce" oauth2 "golang.org/x/oauth2" + types "k8s.io/apimachinery/pkg/types" ) // 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) } +// 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. func (m *MockUpstreamOIDCIdentityProviderI) GetAuthorizationURL() *url.URL { 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)) } +// 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. func (m *MockUpstreamOIDCIdentityProviderI) GetScopes() []string { m.ctrl.T.Helper() diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 5ff4c1a9..270678bc 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -30,7 +30,10 @@ import ( "go.pinniped.dev/pkg/oidcclient/pkce" ) -const promptParamNone = "none" +const ( + promptParamName = "prompt" + promptParamNone = "none" +) func NewHandler( downstreamIssuer string, @@ -51,13 +54,13 @@ func NewHandler( 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 { plog.WarningErr("authorize upstream config", err) return err } - if oidcUpstream != nil { + if idpType == psession.ProviderTypeOIDC { 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. return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) @@ -74,6 +77,7 @@ func NewHandler( return handleAuthRequestForLDAPUpstream(r, w, oauthHelperWithStorage, ldapUpstream, + idpType, ) })) } @@ -83,6 +87,7 @@ func handleAuthRequestForLDAPUpstream( w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, ldapUpstream provider.UpstreamLDAPIdentityProviderI, + idpType psession.ProviderType, ) error { authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper) if !created { @@ -108,7 +113,14 @@ func handleAuthRequestForLDAPUpstream( username = authenticateResponse.User.GetName() 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( @@ -147,6 +159,15 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( 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) if err != nil { // 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( @@ -223,17 +252,20 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( } authCodeOptions := []oauth2.AuthCodeOption{ - oauth2.AccessTypeOffline, nonceValue.Param(), pkceValue.Challenge(), pkceValue.Method(), } - promptParam := r.Form.Get("prompt") + promptParam := r.Form.Get(promptParamName) if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired) } + for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) + } + if csrfFromCookie == "" { // We did not receive an incoming CSRF cookie, so write a new one. err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) @@ -280,8 +312,9 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect( subject string, username string, groups []string, + customSessionData *psession.CustomSessionData, ) error { - openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups) + openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) 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. -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() ldapUpstreams := idpLister.GetLDAPIdentityProviders() adUpstreams := idpLister.GetActiveDirectoryIdentityProviders() switch { case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0: - return nil, nil, httperr.New( + return nil, nil, "", httperr.New( http.StatusUnprocessableEntity, "No upstream providers are configured", ) @@ -362,16 +395,16 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) } plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) - return nil, nil, httperr.New( + return nil, nil, "", httperr.New( http.StatusUnprocessableEntity, "Too many upstream providers are configured (support for multiple upstreams is not yet implemented)", ) case len(oidcUpstreams) == 1: - return oidcUpstreams[0], nil, nil + return oidcUpstreams[0], nil, psession.ProviderTypeOIDC, nil case len(adUpstreams) == 1: - return nil, adUpstreams[0], nil + return nil, adUpstreams[0], psession.ProviderTypeActiveDirectory, nil default: - return nil, ldapUpstreams[0], nil + return nil, ldapUpstreams[0], psession.ProviderTypeLDAP, nil } } diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 8762f9aa..7f61c103 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -30,6 +30,7 @@ import ( "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -38,16 +39,23 @@ import ( func TestAuthorizationEndpoint(t *testing.T) { const ( - oidcUpstreamName = "some-oidc-idp" - oidcPasswordGrantUpstreamName = "some-password-granting-oidc-idp" + oidcUpstreamName = "some-oidc-idp" + oidcUpstreamResourceUID = "oidc-resource-uid" + 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" - oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL - oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" - oidcUpstreamUsername = "test-oidc-pinniped-username" - oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec - oidcUpstreamUsernameClaim = "the-user-claim" - oidcUpstreamGroupsClaim = "the-groups-claim" + oidcUpstreamIssuer = "https://my-upstream-issuer.com" + oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL + oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" + oidcUpstreamUsername = "test-oidc-pinniped-username" + oidcUpstreamPassword = "test-oidc-pinniped-password" //nolint: gosec + oidcUpstreamUsernameClaim = "the-user-claim" + oidcUpstreamGroupsClaim = "the-groups-claim" + oidcPasswordGrantUpstreamRefreshToken = "some-opaque-token" //nolint: gosec downstreamIssuer = "https://my-downstream-issuer.com/some-path" downstreamRedirectURI = "http://127.0.0.1/callback" @@ -146,6 +154,12 @@ func TestAuthorizationEndpoint(t *testing.T) { "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{ "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.", @@ -208,20 +222,22 @@ func TestAuthorizationEndpoint(t *testing.T) { upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth") require.NoError(t, err) - upstreamOIDCIdentityProvider := func() *oidctestutil.TestUpstreamOIDCIdentityProvider { + upstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(oidcUpstreamName). + WithResourceUID(oidcUpstreamResourceUID). WithClientID("some-client-id"). WithAuthorizationURL(*upstreamAuthURL). WithScopes([]string{"scope1", "scope2"}). // the scopes to request when starting the upstream authorization flow WithAllowPasswordGrant(false). - WithPasswordGrantError(errors.New("should not have used password grant on this instance")). - Build() + WithAdditionalAuthcodeParams(map[string]string{}). + WithPasswordGrantError(errors.New("should not have used password grant on this instance")) } passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(oidcPasswordGrantUpstreamName). + WithResourceUID(oidcPasswordGrantUpstreamResourceUID). WithClientID("some-client-id"). WithAuthorizationURL(*upstreamAuthURL). 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("other-claim", "should be ignored"). 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")) } @@ -254,28 +272,39 @@ func TestAuthorizationEndpoint(t *testing.T) { parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL) require.NoError(t, err) + ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + if username == "" || password == "" { + return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") + } + if username == happyLDAPUsername && password == happyLDAPPassword { + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: happyLDAPUsernameFromAuthenticator, + UID: happyLDAPUID, + Groups: happyLDAPGroups, + }, + }, true, nil + } + return nil, false, nil + } + upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: "some-ldap-idp", - URL: parsedUpstreamLDAPURL, - AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { - if username == "" || password == "" { - return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") - } - if username == happyLDAPUsername && password == happyLDAPPassword { - return &authenticator.Response{ - User: &user.DefaultInfo{ - Name: happyLDAPUsernameFromAuthenticator, - UID: happyLDAPUID, - Groups: happyLDAPGroups, - }, - }, true, nil - } - return nil, false, nil - }, + Name: ldapUpstreamName, + ResourceUID: ldapUpstreamResourceUID, + URL: parsedUpstreamLDAPURL, + AuthenticateFunc: ldapAuthenticateFunc, + } + + upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: activeDirectoryUpstreamName, + ResourceUID: activeDirectoryUpstreamResourceUID, + URL: parsedUpstreamLDAPURL, + AuthenticateFunc: ldapAuthenticateFunc, } erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ - Name: "some-ldap-idp", + Name: ldapUpstreamName, + ResourceUID: ldapUpstreamResourceUID, AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { return nil, false, fmt.Errorf("some ldap upstream auth error") }, @@ -387,10 +416,9 @@ func TestAuthorizationEndpoint(t *testing.T) { return encoded } - expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedPrompt string) string { + expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedAdditionalParams map[string]string) string { query := map[string]string{ "response_type": "code", - "access_type": "offline", "scope": "scope1 scope2", "client_id": "some-client-id", "state": expectedUpstreamState, @@ -399,12 +427,35 @@ func TestAuthorizationEndpoint(t *testing.T) { "code_challenge_method": downstreamPKCEChallengeMethod, "redirect_uri": downstreamIssuer + "/callback", } - if expectedPrompt != "" { - query["prompt"] = expectedPrompt + for key, val := range expectedAdditionalParams { + query[key] = val } 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 happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState @@ -452,11 +503,12 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce string wantUnnecessaryStoredRecords int wantPasswordGrantCall *expectedPasswordGrant + wantDownstreamCustomSessionData *psession.CustomSessionData } tests := []testCase{ { 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, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -467,7 +519,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusFound, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -491,6 +543,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { name: "LDAP upstream happy path using GET", @@ -511,10 +564,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, { name: "ActiveDirectory upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: pointer.StringPtr(happyLDAPUsername), @@ -531,10 +585,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, }, { 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, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -545,13 +600,13 @@ func TestAuthorizationEndpoint(t *testing.T) { csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusFound, wantContentType: htmlContentType, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, { name: "OIDC upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -565,7 +620,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "", wantBodyString: "", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantUpstreamStateParamInLocationHeader: true, }, { @@ -590,6 +645,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { name: "LDAP upstream happy path using POST", @@ -612,10 +668,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, { name: "Active Directory upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodPost, path: "/some/path", contentType: "application/x-www-form-urlencoded", @@ -634,10 +691,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, }, { 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, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -651,12 +709,31 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, 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, }, { 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, generatePKCE: happyPKCEGenerator, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -686,13 +763,13 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, // Generated a new CSRF cookie and set it in the response. wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantUpstreamStateParamInLocationHeader: 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -707,7 +784,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client - }, "", ""), ""), + }, "", ""), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -733,6 +810,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, { 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, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -771,7 +850,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "scope": "openid offline_access", - }, "", ""), ""), + }, "", ""), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -834,7 +913,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream password for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: pointer.StringPtr(happyLDAPUsername), @@ -858,7 +937,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "wrong upstream username for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: pointer.StringPtr("wrong-username"), @@ -882,7 +961,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing upstream username on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: pointer.StringPtr(happyLDAPUsername), @@ -916,6 +995,32 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), 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", 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPath, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{ "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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1031,7 +1136,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1040,7 +1145,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1077,7 +1182,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), customUsernameHeader: pointer.StringPtr(happyLDAPUsername), @@ -1138,7 +1243,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1175,7 +1280,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request using Active Directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusFound, @@ -1185,7 +1290,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing client id in request using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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 // 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1449,7 +1554,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( map[string]string{"prompt": "none login", "scope": "email"}, "", "", - ), ""), + ), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -1474,6 +1579,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, 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", @@ -1518,6 +1625,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, 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", @@ -1543,6 +1651,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, 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", @@ -1569,6 +1678,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, 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", @@ -1596,6 +1706,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, 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", @@ -1655,6 +1766,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1948,7 +2063,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { 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, generatePKCE: happyPKCEGenerator, 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", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1976,7 +2091,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating CSRF token using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: sadCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1990,7 +2105,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating nonce using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: sadNonceGenerator, @@ -2004,7 +2119,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating PKCE using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: sadPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2027,7 +2142,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { 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, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -2054,7 +2169,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { 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, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -2063,7 +2178,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { 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, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -2072,7 +2187,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PUT is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPut, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -2081,7 +2196,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PATCH is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPatch, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -2090,7 +2205,7 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "DELETE is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodDelete, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -2166,6 +2281,7 @@ func TestAuthorizationEndpoint(t *testing.T) { test.wantDownstreamNonce, downstreamClientID, test.wantDownstreamRedirectURI, + test.wantDownstreamCustomSessionData, ) default: require.Empty(t, rsp.Header().Values("Location")) @@ -2239,6 +2355,7 @@ func TestAuthorizationEndpoint(t *testing.T) { WithClientID("some-other-new-client-id"). WithAuthorizationURL(*upstreamAuthURL). WithScopes([]string{"some-other-new-scope1", "some-other-new-scope2"}). + WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123"}). Build() idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(newProviderSettings)}) @@ -2246,7 +2363,8 @@ func TestAuthorizationEndpoint(t *testing.T) { test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), map[string]string{ "response_type": "code", - "access_type": "offline", + "prompt": "consent", + "abc": "123", "scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation "client_id": "some-other-new-client-id", // updated expectation "state": expectedUpstreamStateParam( diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index b168e4b9..e99d1678 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -19,6 +19,7 @@ import ( "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" ) func NewHandler( @@ -68,12 +69,27 @@ func NewHandler( 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) if err != nil { 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) if err != nil { diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 9cc5779e..6bfcf340 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -18,6 +18,7 @@ import ( "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -25,9 +26,11 @@ import ( ) const ( - happyUpstreamIDPName = "upstream-idp-name" + happyUpstreamIDPName = "upstream-idp-name" + happyUpstreamIDPResourceUID = "upstream-uid" oidcUpstreamIssuer = "https://my-upstream-issuer.com" + oidcUpstreamRefreshToken = "test-refresh-token" oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL oidcUpstreamSubjectQueryEscaped = "abc123-some+guid" oidcUpstreamUsername = "test-pinniped-username" @@ -69,7 +72,13 @@ var ( "code_challenge_method": []string{downstreamPKCEChallengeMethod}, "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) { @@ -130,6 +139,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce string wantDownstreamPKCEChallenge string wantDownstreamPKCEChallengeMethod string + wantDownstreamCustomSessionData *psession.CustomSessionData wantAuthcodeExchangeCall *expectedAuthcodeExchange }{ @@ -157,6 +167,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -179,6 +190,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -203,6 +215,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -227,6 +240,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -253,6 +267,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -280,6 +295,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -302,6 +318,34 @@ func TestCallbackEndpoint(t *testing.T) { 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", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( @@ -339,6 +383,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -363,6 +408,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -387,6 +433,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -537,6 +584,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -563,6 +611,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -660,6 +709,7 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamNonce: downstreamNonce, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ performedByUpstreamName: happyUpstreamIDPName, args: happyExchangeAndValidateTokensArgs, @@ -907,6 +957,7 @@ func TestCallbackEndpoint(t *testing.T) { test.wantDownstreamNonce, downstreamClientID, downstreamRedirectURI, + test.wantDownstreamCustomSessionData, ) // Otherwise, expect an empty response body. @@ -933,6 +984,7 @@ func TestCallbackEndpoint(t *testing.T) { test.wantDownstreamNonce, downstreamClientID, downstreamRedirectURI, + test.wantDownstreamCustomSessionData, ) } }) @@ -1036,6 +1088,7 @@ func (b *upstreamStateParamBuilder) WithStateVersion(version string) *upstreamSt func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(happyUpstreamIDPName). + WithResourceUID(happyUpstreamIDPResourceUID). WithClientID("some-client-id"). WithScopes([]string{"scope1", "scope2"}). WithUsernameClaim(oidcUpstreamUsernameClaim). @@ -1046,6 +1099,7 @@ func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { WithIDTokenClaim(oidcUpstreamGroupsClaim, oidcUpstreamGroupMembership). WithIDTokenClaim("other-claim", "should be ignored"). WithAllowPasswordGrant(false). + WithRefreshToken(oidcUpstreamRefreshToken). WithPasswordGrantError(errors.New("the callback endpoint should not use password grants")) } diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/oidc/downstreamsession/downstream_session.go index bd29646e..0fee5a78 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/oidc/downstreamsession/downstream_session.go @@ -36,7 +36,7 @@ const ( ) // 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() openIDSession := &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ @@ -46,6 +46,7 @@ func MakeDownstreamSession(subject string, username string, groups []string) *ps AuthTime: now, }, }, + Custom: custom, } if groups == nil { groups = []string{} diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index ef57f6e5..6d0af8ec 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -9,6 +9,7 @@ import ( "sync" "golang.org/x/oauth2" + "k8s.io/apimachinery/pkg/types" "go.pinniped.dev/internal/authenticators" "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() string + // GetResourceUID returns the Kubernetes resource ID + GetResourceUID() types.UID + // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. GetAuthorizationURL() *url.URL @@ -42,6 +46,9 @@ type UpstreamOIDCIdentityProviderI interface { // flow with this upstream provider. When false, it should not be allowed. 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 // 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) @@ -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. 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. authenticators.UserAuthenticator } diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 2731b488..e78b9126 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -264,6 +264,7 @@ func TestManager(t *testing.T) { "groups": "test-group1", }, }, + RefreshToken: &oidctypes.RefreshToken{Token: "some-opaque-token"}, }, nil }, }).Build() diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index c8a36153..262f5756 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -1374,7 +1374,7 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques Subject: "", // 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{ UpstreamRefreshToken: "starting-fake-refresh-token", }, diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go index 334dce06..72ea3bdb 100644 --- a/internal/psession/pinniped_session.go +++ b/internal/psession/pinniped_session.go @@ -10,6 +10,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "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. @@ -18,20 +19,20 @@ type PinnipedSession struct { Fosite *openid.DefaultSession `json:"fosite,omitempty"` // Custom Pinniped extensions to the session data. - Custom *PinnipedSessionData `json:"custom,omitempty"` + Custom *CustomSessionData `json:"custom,omitempty"` } 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. -type PinnipedSessionData struct { +type CustomSessionData struct { // 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 // 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 // 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. // 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. // Used during a downstream refresh to decide which upstream to refresh. - ProviderType string `json:"providerType"` + ProviderType ProviderType `json:"providerType"` // Only used when ProviderType == "oidc". 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. type OIDCSessionData struct { UpstreamRefreshToken string `json:"upstreamRefreshToken"` @@ -58,7 +67,7 @@ func NewPinnipedSession() *PinnipedSession { Claims: &jwt.IDTokenClaims{}, Headers: &jwt.Headers{}, }, - Custom: &PinnipedSessionData{}, + Custom: &CustomSessionData{}, } } diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 5ba9512a..3f8c3f57 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -20,6 +20,7 @@ import ( "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/client-go/kubernetes/fake" v1 "k8s.io/client-go/kubernetes/typed/core/v1" @@ -59,12 +60,17 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct { type TestUpstreamLDAPIdentityProvider struct { Name string + ResourceUID types.UID URL *url.URL AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) } var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} +func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID { + return u.ResourceUID +} + func (u *TestUpstreamLDAPIdentityProvider) GetName() string { return u.Name } @@ -78,13 +84,15 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { } type TestUpstreamOIDCIdentityProvider struct { - Name string - ClientID string - AuthorizationURL url.URL - UsernameClaim string - GroupsClaim string - Scopes []string - AllowPasswordGrant bool + Name string + ClientID string + ResourceUID types.UID + AuthorizationURL url.URL + UsernameClaim string + GroupsClaim string + Scopes []string + AdditionalAuthcodeParams map[string]string + AllowPasswordGrant bool ExchangeAuthcodeAndValidateTokensFunc func( ctx context.Context, @@ -105,6 +113,16 @@ type TestUpstreamOIDCIdentityProvider struct { 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 { return u.Name } @@ -303,16 +321,19 @@ func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { } type TestUpstreamOIDCIdentityProviderBuilder struct { - name string - clientID string - scopes []string - idToken map[string]interface{} - usernameClaim string - groupsClaim string - authorizationURL url.URL - allowPasswordGrant bool - authcodeExchangeErr error - passwordGrantErr error + name string + resourceUID types.UID + clientID string + scopes []string + idToken map[string]interface{} + refreshToken *oidctypes.RefreshToken + usernameClaim string + groupsClaim string + authorizationURL url.URL + additionalAuthcodeParams map[string]string + allowPasswordGrant bool + authcodeExchangeErr error + passwordGrantErr error } func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { @@ -320,6 +341,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUp return u } +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamOIDCIdentityProviderBuilder { + u.resourceUID = value + return u +} + func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder { u.clientID = value return u @@ -373,6 +399,26 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutIDTokenClaim(claim stri 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 { u.authcodeExchangeErr = err return u @@ -385,24 +431,26 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPasswordGrantError(err err func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { return &TestUpstreamOIDCIdentityProvider{ - Name: u.name, - ClientID: u.clientID, - UsernameClaim: u.usernameClaim, - GroupsClaim: u.groupsClaim, - Scopes: u.scopes, - AllowPasswordGrant: u.allowPasswordGrant, - AuthorizationURL: u.authorizationURL, + Name: u.name, + ClientID: u.clientID, + ResourceUID: u.resourceUID, + UsernameClaim: u.usernameClaim, + GroupsClaim: u.groupsClaim, + Scopes: u.scopes, + AllowPasswordGrant: u.allowPasswordGrant, + AuthorizationURL: u.authorizationURL, + AdditionalAuthcodeParams: u.additionalAuthcodeParams, ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { if u.authcodeExchangeErr != nil { 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) { if u.passwordGrantErr != nil { 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, wantDownstreamClientID string, wantDownstreamRedirectURI string, + wantCustomSessionData *psession.CustomSessionData, ) { t.Helper() @@ -513,6 +562,7 @@ func RequireAuthCodeRegexpMatch( wantDownstreamRequestedScopes, wantDownstreamClientID, wantDownstreamRedirectURI, + wantCustomSessionData, ) // One PKCE should have been stored. @@ -563,6 +613,7 @@ func validateAuthcodeStorage( wantDownstreamRequestedScopes []string, wantDownstreamClientID string, wantDownstreamRedirectURI string, + wantCustomSessionData *psession.CustomSessionData, ) (*fosite.Request, *psession.PinnipedSession) { t.Helper() @@ -634,6 +685,9 @@ func validateAuthcodeStorage( require.Empty(t, actualClaims.AuthenticationContextClassReference) require.Empty(t, actualClaims.AuthenticationMethodsReference) + // Check that the custom Pinniped session data matches. + require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom) + return storedRequestFromAuthcode, storedSessionFromAuthcode } diff --git a/internal/testutil/psession.go b/internal/testutil/psession.go index d6f4bcc8..28e65968 100644 --- a/internal/testutil/psession.go +++ b/internal/testutil/psession.go @@ -23,7 +23,7 @@ func NewFakePinnipedSession() *psession.PinnipedSession { Username: "snorlax", Subject: "panda", }, - Custom: &psession.PinnipedSessionData{ + Custom: &psession.CustomSessionData{ ProviderUID: "fake-provider-uid", ProviderType: "fake-provider-type", ProviderName: "fake-provider-name", diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 1a147b43..1baab58b 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -20,6 +20,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/google/uuid" + "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/utils/trace" @@ -82,6 +83,9 @@ type ProviderConfig struct { // Name is the unique name of this upstream LDAP IDP. 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, // the default LDAP port will be used. Host string @@ -265,6 +269,10 @@ func (p *Provider) GetName() string { 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". // 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. diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index 3a249c1d..17e70915 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -14,6 +14,7 @@ import ( coreosoidc "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "go.pinniped.dev/internal/httputil/httperr" @@ -31,19 +32,29 @@ func New(config *oauth2.Config, provider *coreosoidc.Provider, client *http.Clie // ProviderConfig holds the active configuration of an upstream OIDC provider. type ProviderConfig struct { - Name string - UsernameClaim string - GroupsClaim string - Config *oauth2.Config - Client *http.Client - AllowPasswordGrant bool - Provider interface { + Name string + ResourceUID types.UID + UsernameClaim string + GroupsClaim string + Config *oauth2.Config + Client *http.Client + AllowPasswordGrant bool + AdditionalAuthcodeParams map[string]string + Provider interface { Verifier(*coreosoidc.Config) *coreosoidc.IDTokenVerifier Claims(v interface{}) error UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*coreosoidc.UserInfo, error) } } +func (p *ProviderConfig) GetResourceUID() types.UID { + return p.ResourceUID +} + +func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string { + return p.AdditionalAuthcodeParams +} + func (p *ProviderConfig) GetName() string { return p.Name } From 79ca1d7fb053fc6c2cfc1d96233b7964312dca38 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 13 Oct 2021 12:31:20 -0700 Subject: [PATCH 04/19] Perform an upstream refresh during downstream refresh for OIDC upstreams - If the upstream refresh fails, then fail the downstream refresh - If the upstream refresh returns an ID token, then validate it (we use its claims in the future, but not in this commit) - If the upstream refresh returns a new refresh token, then save it into the user's session in storage - Pass the provider cache into the token handler so it can use the cached providers to perform upstream refreshes - Handle unexpected errors in the token handler where the user's session does not contain the expected data. These should not be possible in practice unless someone is manually editing the storage, but handle them anyway just to be safe. - Refactor to share the refresh code between the CLI and the token endpoint by moving it into the UpstreamOIDCIdentityProviderI interface, since the token endpoint needed it to be part of that interface anyway --- .../mockupstreamoidcidentityprovider.go | 15 + internal/oidc/oidc.go | 4 +- .../provider/dynamic_upstream_idp_provider.go | 8 + internal/oidc/provider/manager/manager.go | 1 + internal/oidc/token/token_handler.go | 127 +++ internal/oidc/token/token_handler_test.go | 775 +++++++++++++++--- .../testutil/oidctestutil/oidctestutil.go | 180 +++- internal/upstreamoidc/upstreamoidc.go | 14 +- internal/upstreamoidc/upstreamoidc_test.go | 166 ++++ pkg/oidcclient/login.go | 6 +- pkg/oidcclient/login_test.go | 25 +- 11 files changed, 1195 insertions(+), 126 deletions(-) diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index dfbe785b..fd8b7dd9 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -200,6 +200,21 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) PasswordCredentialsGran return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCredentialsGrantAndValidateTokens", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).PasswordCredentialsGrantAndValidateTokens), arg0, arg1, arg2) } +// PerformRefresh mocks base method. +func (m *MockUpstreamOIDCIdentityProviderI) PerformRefresh(arg0 context.Context, arg1 string) (*oauth2.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PerformRefresh", arg0, arg1) + ret0, _ := ret[0].(*oauth2.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PerformRefresh indicates an expected call of PerformRefresh. +func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) PerformRefresh(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PerformRefresh", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).PerformRefresh), arg0, arg1) +} + // ValidateToken mocks base method. func (m *MockUpstreamOIDCIdentityProviderI) ValidateToken(arg0 context.Context, arg1 *oauth2.Token, arg2 nonce.Nonce) (*oidctypes.Token, error) { m.ctrl.T.Helper() diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index b70e0309..6c3c1918 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -210,8 +210,6 @@ func FositeOauth2Helper( // The default is to support all prompt values from the spec. // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest - // We'll make a best effort to support these by passing the value of this prompt param to the upstream IDP - // and rely on its implementation of this param. AllowedPromptValues: nil, // Use the fosite default to make it more likely that off the shelf OIDC clients can work with the supervisor. @@ -232,7 +230,7 @@ func FositeOauth2Helper( compose.OpenIDConnectExplicitFactory, compose.OpenIDConnectRefreshFactory, compose.OAuth2PKCEFactory, - TokenExchangeFactory, + TokenExchangeFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type ) provider.(*fosite.Fosite).FormPostHTMLTemplate = formposthtml.Template() return provider diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index 6d0af8ec..9ece5f4d 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -63,6 +63,14 @@ type UpstreamOIDCIdentityProviderI interface { redirectURI string, ) (*oidctypes.Token, error) + // PerformRefresh will call the provider's token endpoint to perform a refresh grant. The provider may or may not + // return a new ID or refresh token in the response. If it returns an ID token, then use ValidateRefresh to + // validate the ID token. + PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) + + // ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response + // into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated + // tokens, or an error. ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index ea1d2d62..708d4855 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -130,6 +130,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs ) m.providerHandlers[(issuerHostWithPath + oidc.TokenEndpointPath)] = token.NewHandler( + m.upstreamIDPs, oauthHelperWithKubeStorage, ) diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index 72222367..724ee0aa 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -5,17 +5,36 @@ package token import ( + "context" "net/http" "github.com/ory/fosite" + "github.com/ory/x/errorsx" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" ) +var ( + errMissingUpstreamSessionInternalError = &fosite.RFC6749Error{ + ErrorField: "error", + DescriptionField: "There was an internal server error.", + HintField: "Required upstream data not found in session.", + CodeField: http.StatusInternalServerError, + } + + errUpstreamRefreshError = &fosite.RFC6749Error{ + ErrorField: "error", + DescriptionField: "Error during upstream refresh.", + CodeField: http.StatusUnauthorized, + } +) + func NewHandler( + idpLister oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider, ) http.Handler { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { @@ -27,6 +46,20 @@ func NewHandler( return nil } + // Check if we are performing a refresh grant. + if accessRequest.GetGrantTypes().ExactOne("refresh_token") { + // The above call to NewAccessRequest has loaded the session from storage into the accessRequest variable. + // The session, requested scopes, and requested audience from the original authorize request was retrieved + // from the Kube storage layer and added to the accessRequest. Additionally, the audience and scopes may + // have already been granted on the accessRequest. + err = upstreamRefresh(r.Context(), accessRequest, idpLister) + if err != nil { + plog.Info("upstream refresh error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAccessError(w, accessRequest, err) + return nil + } + } + accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest) if err != nil { plog.Info("token response error", oidc.FositeErrorForLog(err)...) @@ -39,3 +72,97 @@ func NewHandler( return nil }) } + +func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error { + session := accessRequest.GetSession().(*psession.PinnipedSession) + customSessionData := session.Custom + if customSessionData == nil { + return errorsx.WithStack(errMissingUpstreamSessionInternalError) + } + providerName := customSessionData.ProviderName + providerUID := customSessionData.ProviderUID + if providerUID == "" || providerName == "" { + return errorsx.WithStack(errMissingUpstreamSessionInternalError) + } + + switch customSessionData.ProviderType { + case psession.ProviderTypeOIDC: + return upstreamOIDCRefresh(ctx, customSessionData, providerCache) + case psession.ProviderTypeLDAP: + // upstream refresh not yet implemented for LDAP, so do nothing + case psession.ProviderTypeActiveDirectory: + // upstream refresh not yet implemented for AD, so do nothing + default: + return errorsx.WithStack(errMissingUpstreamSessionInternalError) + } + + return nil +} + +func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error { + if s.OIDC == nil || s.OIDC.UpstreamRefreshToken == "" { + return errorsx.WithStack(errMissingUpstreamSessionInternalError) + } + + p, err := findOIDCProviderByNameAndValidateUID(s, providerCache) + if err != nil { + return err + } + + plog.Debug("attempting upstream refresh request", + "providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID) + + refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken) + if err != nil { + return errorsx.WithStack(errUpstreamRefreshError.WithHintf( + "Upstream refresh failed using provider %q of type %q.", + s.ProviderName, s.ProviderType).WithWrap(err)) + } + + // Upstream refresh may or may not return a new ID token. From the spec: + // "the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token." + // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse + _, hasIDTok := refreshedTokens.Extra("id_token").(string) + if hasIDTok { + // The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at + // least some providers do not include one, so we skip the nonce validation here (but not other validations). + _, err = p.ValidateToken(ctx, refreshedTokens, "") + if err != nil { + return errorsx.WithStack(errUpstreamRefreshError.WithHintf( + "Upstream refresh returned an invalid ID token using provider %q of type %q.", + s.ProviderName, s.ProviderType).WithWrap(err)) + } + } else { + plog.Debug("upstream refresh request did not return a new ID token", + "providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID) + } + + // Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in + // the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding + // overwriting the old one. + if refreshedTokens.RefreshToken != "" { + plog.Debug("upstream refresh request did not return a new refresh token", + "providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID) + s.OIDC.UpstreamRefreshToken = refreshedTokens.RefreshToken + } + + return nil +} + +func findOIDCProviderByNameAndValidateUID( + s *psession.CustomSessionData, + providerCache oidc.UpstreamIdentityProvidersLister, +) (provider.UpstreamOIDCIdentityProviderI, error) { + for _, p := range providerCache.GetOIDCIdentityProviders() { + if p.GetName() == s.ProviderName { + if p.GetResourceUID() != s.ProviderUID { + return nil, errorsx.WithStack(errUpstreamRefreshError.WithHintf( + "Provider %q of type %q from upstream session data has changed its resource UID since authentication.", + s.ProviderName, s.ProviderType)) + } + return p, nil + } + } + return nil, errorsx.WithStack(errUpstreamRefreshError. + WithHintf("Provider %q of type %q from upstream session data was not found.", s.ProviderName, s.ProviderType)) +} diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 262f5756..968944d7 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -23,12 +23,13 @@ import ( "time" "github.com/ory/fosite" - "github.com/ory/fosite/handler/oauth2" + fositeoauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/pkce" "github.com/ory/fosite/token/jwt" "github.com/pkg/errors" "github.com/stretchr/testify/require" + "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" josejwt "gopkg.in/square/go-jose.v2/jwt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -44,8 +45,10 @@ import ( "go.pinniped.dev/internal/fositestorage/refreshtoken" "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -179,6 +182,13 @@ var ( } `) + pinnipedUpstreamSessionDataNotFoundErrorBody = here.Doc(` + { + "error": "error", + "error_description": "There was an internal server error. Required upstream data not found in session." + } + `) + happyAuthRequest = &http.Request{ Form: url.Values{ "response_type": {"code"}, @@ -206,12 +216,25 @@ var ( } ) +type expectedUpstreamRefresh struct { + performedByUpstreamName string + args *oidctestutil.PerformRefreshArgs +} + +type expectedUpstreamValidateTokens struct { + performedByUpstreamName string + args *oidctestutil.ValidateTokenArgs +} + type tokenEndpointResponseExpectedValues struct { - wantStatus int - wantSuccessBodyFields []string - wantErrorResponseBody string - wantRequestedScopes []string - wantGrantedScopes []string + wantStatus int + wantSuccessBodyFields []string + wantErrorResponseBody string + wantRequestedScopes []string + wantGrantedScopes []string + wantUpstreamOIDCRefreshCall *expectedUpstreamRefresh + wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens + wantCustomSessionDataStored *psession.CustomSessionData } type authcodeExchangeInputs struct { @@ -222,13 +245,9 @@ type authcodeExchangeInputs struct { s fositestoragei.AllFositeStorage, authCode string, ) - makeOathHelper func( - t *testing.T, - authRequest *http.Request, - store fositestoragei.AllFositeStorage, - ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) - - want tokenEndpointResponseExpectedValues + makeOathHelper OauthHelperFactoryFunc + customSessionData *psession.CustomSessionData + want tokenEndpointResponseExpectedValues } func TestTokenEndpoint(t *testing.T) { @@ -520,7 +539,8 @@ func TestTokenEndpoint(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - exchangeAuthcodeForTokens(t, test.authcodeExchange) + // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. + exchangeAuthcodeForTokens(t, test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build()) }) } } @@ -549,7 +569,9 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) { t.Parallel() // First call - should be successful. - subject, rsp, authCode, _, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange) + // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. + subject, rsp, authCode, _, secrets, oauthStore := exchangeAuthcodeForTokens(t, + test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build()) var parsedResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody)) @@ -573,9 +595,11 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) { requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore) // This was previously invalidated by the first request, so it remains invalidated requireInvalidPKCEStorage(t, authCode, oauthStore) - // Fosite never cleans up OpenID Connect session storage, so it is still there + // Fosite never cleans up OpenID Connect session storage, so it is still there. + // Note that customSessionData is only relevant to refresh grant, so we leave it as nil for this + // authcode exchange test, even though in practice it would actually be in the session. requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, - test.authcodeExchange.want.wantRequestedScopes, test.authcodeExchange.want.wantGrantedScopes) + test.authcodeExchange.want.wantRequestedScopes, test.authcodeExchange.want.wantGrantedScopes, nil) // Check that the access token and refresh token storage were both deleted, and the number of other storage objects did not change. testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) @@ -602,6 +626,7 @@ func TestTokenExchange(t *testing.T) { }, want: successfulAuthCodeExchange, } + tests := []struct { name string @@ -742,7 +767,9 @@ func TestTokenExchange(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, test.authcodeExchange) + // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. + subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, + test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().Build()) var parsedAuthcodeExchangeResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) @@ -845,80 +872,219 @@ type refreshRequestInputs struct { } func TestRefreshGrant(t *testing.T) { + const ( + oidcUpstreamName = "some-oidc-idp" + oidcUpstreamResourceUID = "oidc-resource-uid" + oidcUpstreamType = "oidc" + oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token" + oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token" + oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token" + ) + + // The below values are funcs so every test can have its own copy of the objects, to avoid data races + // in these parallel tests. + + upstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { + return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithName(oidcUpstreamName). + WithResourceUID(oidcUpstreamResourceUID) + } + + initialUpstreamOIDCCustomSessionData := func() *psession.CustomSessionData { + return &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{ + UpstreamRefreshToken: oidcUpstreamInitialRefreshToken, + }, + } + } + + upstreamOIDCCustomSessionDataWithNewRefreshToken := func(newRefreshToken string) *psession.CustomSessionData { + sessionData := initialUpstreamOIDCCustomSessionData() + sessionData.OIDC.UpstreamRefreshToken = newRefreshToken + return sessionData + } + + happyUpstreamRefreshCall := func() *expectedUpstreamRefresh { + return &expectedUpstreamRefresh{ + performedByUpstreamName: oidcUpstreamName, + args: &oidctestutil.PerformRefreshArgs{ + Ctx: nil, // this will be filled in with the actual request context by the test below + RefreshToken: oidcUpstreamInitialRefreshToken, + }, + } + } + + happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens { + return &expectedUpstreamValidateTokens{ + performedByUpstreamName: oidcUpstreamName, + args: &oidctestutil.ValidateTokenArgs{ + Ctx: nil, // this will be filled in with the actual request context by the test below + Tok: expectedTokens, + ExpectedIDTokenNonce: "", // always expect empty string + }, + } + } + + happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues { + want := tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access"}, + wantGrantedScopes: []string{"openid", "offline_access"}, + wantCustomSessionDataStored: wantCustomSessionDataStored, + } + return want + } + + happyRefreshTokenResponseForOpenIDAndOfflineAccess := func(wantCustomSessionDataStored *psession.CustomSessionData, expectToValidateToken *oauth2.Token) tokenEndpointResponseExpectedValues { + // Should always have some custom session data stored. The other expectations happens to be the + // same as the same values as the authcode exchange case. + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) + // Should always try to perform an upstream refresh. + want.wantUpstreamOIDCRefreshCall = happyUpstreamRefreshCall() + // Should only try to ValidateToken when there was an id token returned by the upstream refresh. + if expectToValidateToken != nil { + want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken) + } + return want + } + + refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token { + return &oauth2.Token{ + AccessToken: "fake-refreshed-access-token", + TokenType: "Bearer", + RefreshToken: oidcUpstreamRefreshedRefreshToken, + Expiry: time.Date(2050, 1, 1, 1, 1, 1, 1, time.UTC), + } + } + + refreshedUpstreamTokensWithIDAndRefreshTokens := func() *oauth2.Token { + return refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(). + WithExtra(map[string]interface{}{"id_token": oidcUpstreamRefreshedIDToken}) + } + + refreshedUpstreamTokensWithIDTokenWithoutRefreshToken := func() *oauth2.Token { + tokens := refreshedUpstreamTokensWithIDAndRefreshTokens() + tokens.RefreshToken = "" // remove the refresh token + return tokens + } + tests := []struct { name string + idps *oidctestutil.UpstreamIDPListerBuilder authcodeExchange authcodeExchangeInputs refreshRequest refreshRequestInputs }{ { - name: "happy path refresh grant with ID token", + name: "happy path refresh grant with openid scope granted (id token returned)", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), }, refreshRequest: refreshRequestInputs{ - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }}, + want: happyRefreshTokenResponseForOpenIDAndOfflineAccess( + upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + refreshedUpstreamTokensWithIDAndRefreshTokens(), + ), + }, }, { - name: "happy path refresh grant without ID token", + name: "happy path refresh grant without openid scope granted (no id token returned)", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"offline_access"}, - wantGrantedScopes: []string{"offline_access"}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"offline_access"}, + wantGrantedScopes: []string{"offline_access"}, + wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(), }, }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"offline_access"}, - wantGrantedScopes: []string{"offline_access"}, - }}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"offline_access"}, + wantGrantedScopes: []string{"offline_access"}, + wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), + wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + }, + }, + }, + { + name: "happy path refresh grant when the upstream refresh does not return a new ID token", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), + }, + refreshRequest: refreshRequestInputs{ + want: happyRefreshTokenResponseForOpenIDAndOfflineAccess( + upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + nil, // expect ValidateToken is *not* called + ), + }, + }, + { + name: "happy path refresh grant when the upstream refresh does not return a new refresh token", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDTokenWithoutRefreshToken()).Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), + }, + refreshRequest: refreshRequestInputs{ + want: happyRefreshTokenResponseForOpenIDAndOfflineAccess( + initialUpstreamOIDCCustomSessionData(), // still has the initial refresh token stored + refreshedUpstreamTokensWithIDTokenWithoutRefreshToken(), + ), + }, }, { name: "when the refresh request adds a new scope to the list of requested scopes then it is ignored", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), }, refreshRequest: refreshRequestInputs{ modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) { r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid some-other-scope-not-from-auth-request").ReadCloser() }, - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }}, + want: happyRefreshTokenResponseForOpenIDAndOfflineAccess( + upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + refreshedUpstreamTokensWithIDAndRefreshTokens(), + ), + }, }, { name: "when the refresh request removes a scope which was originally granted from the list of requested scopes then it is granted anyway", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience") }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, - wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(), }, }, refreshRequest: refreshRequestInputs{ @@ -926,43 +1092,47 @@ func TestRefreshGrant(t *testing.T) { r.Body = happyRefreshRequestBody(refreshToken).WithScope("openid").ReadCloser() // do not ask for "pinniped:request-audience" again }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, - wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, - }}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), + wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + }, + }, }, { name: "when the refresh request does not include a scope param then it gets all the same scopes as the original authorization request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), }, refreshRequest: refreshRequestInputs{ modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) { r.Body = happyRefreshRequestBody(refreshToken).WithScope("").ReadCloser() }, - want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"openid", "offline_access"}, - wantGrantedScopes: []string{"openid", "offline_access"}, - }}, + want: happyRefreshTokenResponseForOpenIDAndOfflineAccess( + upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), + refreshedUpstreamTokensWithIDAndRefreshTokens(), + ), + }, }, { name: "when a bad refresh token is sent in the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"offline_access"}, - wantGrantedScopes: []string{"offline_access"}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"offline_access"}, + wantGrantedScopes: []string{"offline_access"}, + wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(), }, }, refreshRequest: refreshRequestInputs{ @@ -972,17 +1142,21 @@ func TestRefreshGrant(t *testing.T) { want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusBadRequest, wantErrorResponseBody: fositeInvalidAuthCodeErrorBody, - }}, + }, + }, }, { name: "when the access token is sent as if it were a refresh token", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"offline_access"}, - wantGrantedScopes: []string{"offline_access"}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"offline_access"}, + wantGrantedScopes: []string{"offline_access"}, + wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(), }, }, refreshRequest: refreshRequestInputs{ @@ -992,17 +1166,21 @@ func TestRefreshGrant(t *testing.T) { want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusBadRequest, wantErrorResponseBody: fositeInvalidAuthCodeErrorBody, - }}, + }, + }, }, { name: "when the wrong client ID is included in the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") }, want: tokenEndpointResponseExpectedValues{ - wantStatus: http.StatusOK, - wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, - wantRequestedScopes: []string{"offline_access"}, - wantGrantedScopes: []string{"offline_access"}, + wantStatus: http.StatusOK, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"offline_access"}, + wantGrantedScopes: []string{"offline_access"}, + wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(), }, }, refreshRequest: refreshRequestInputs{ @@ -1012,7 +1190,301 @@ func TestRefreshGrant(t *testing.T) { want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusUnauthorized, wantErrorResponseBody: fositeInvalidClientErrorBody, - }}, + }, + }, + }, + { + name: "when there is no custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: nil, // this should not happen in practice + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(nil), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is no provider name in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: "", // this should not happen in practice + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: "", // this should not happen in practice + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is no provider UID in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: "", // this should not happen in practice + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: "", // this should not happen in practice + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is no provider type in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: "", // this should not happen in practice + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: "", // this should not happen in practice + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is an illegal provider type in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: "not-an-allowed-provider-type", // this should not happen in practice + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: "not-an-allowed-provider-type", // this should not happen in practice + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is no OIDC-specific data in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: nil, // this should not happen in practice + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: nil, // this should not happen in practice + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when there is no OIDC refresh token in custom session data found in the session storage during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{ + UpstreamRefreshToken: "", // this should not happen in practice + }, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{ + UpstreamRefreshToken: "", // this should not happen in practice + }, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusInternalServerError, + wantErrorResponseBody: pinnipedUpstreamSessionDataNotFoundErrorBody, + }, + }, + }, + { + name: "when the provider in the session storage is not found due to its name during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: "this-name-will-not-be-found", // this could happen if the OIDCIdentityProvider was deleted since original login + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: "this-name-will-not-be-found", // this could happen if the OIDCIdentityProvider was deleted since original login + ProviderUID: oidcUpstreamResourceUID, + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Provider 'this-name-will-not-be-found' of type 'oidc' from upstream session data was not found." + } + `), + }, + }, + }, + { + name: "when the provider in the session storage is found but has the wrong resource UID during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: &psession.CustomSessionData{ + ProviderName: oidcUpstreamName, + ProviderUID: "this is the wrong uid", // this could happen if the OIDCIdentityProvider was deleted and recreated at the same name since original login + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + &psession.CustomSessionData{ // want the initial customSessionData to be unmodified + ProviderName: oidcUpstreamName, + ProviderUID: "this is the wrong uid", // this could happen if the OIDCIdentityProvider was deleted and recreated at the same name since original login + ProviderType: oidcUpstreamType, + OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamInitialRefreshToken}, + }, + ), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Provider 'some-oidc-idp' of type 'oidc' from upstream session data has changed its resource UID since authentication." + } + `), + }, + }, + }, + { + name: "when the upstream refresh fails during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). + WithPerformRefreshError(errors.New("some upstream refresh error")).Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh failed using provider 'some-oidc-idp' of type 'oidc'." + } + `), + }, + }, + }, + { + name: "when the upstream refresh returns an invalid ID token during the refresh request", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). + WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). + // This is the current format of the errors returned by the production code version of ValidateToken, see ValidateToken in upstreamoidc.go + WithValidateTokenError(httperr.Wrap(http.StatusBadRequest, "some validate error", errors.New("some validate cause"))). + Build()), + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamOIDCCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") }, + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()), + }, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), + wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token using provider 'some-oidc-idp' of type 'oidc'." + } + `), + }, + }, }, } for _, test := range tests { @@ -1021,10 +1493,15 @@ func TestRefreshGrant(t *testing.T) { t.Parallel() // First exchange the authcode for tokens, including a refresh token. - subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange) + subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build()) var parsedAuthcodeExchangeResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) + // Performing an authcode exchange should not have caused any upstream refresh, which should only + // happen during a downstream refresh. + test.idps.RequireExactlyZeroCallsToPerformRefresh(t) + test.idps.RequireExactlyZeroCallsToValidateToken(t) + // Wait one second before performing the refresh so we can see that the refreshed ID token has new issued // at and expires at dates which are newer than the old tokens. // If this gets too annoying in terms of making our test suite slower then we can remove it and adjust @@ -1033,8 +1510,10 @@ func TestRefreshGrant(t *testing.T) { // Send the refresh token back and preform a refresh. firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string) + require.NotEmpty(t, firstRefreshToken) + reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context") req := httptest.NewRequest("POST", "/path/shouldn't/matter", - happyRefreshRequestBody(firstRefreshToken).ReadCloser()) + happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if test.refreshRequest.modifyTokenRequest != nil { test.refreshRequest.modifyTokenRequest(req, firstRefreshToken, parsedAuthcodeExchangeResponseBody["access_token"].(string)) @@ -1045,11 +1524,45 @@ func TestRefreshGrant(t *testing.T) { t.Logf("second response: %#v", refreshResponse) t.Logf("second response body: %q", refreshResponse.Body.String()) + // Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh. + if test.refreshRequest.want.wantUpstreamOIDCRefreshCall != nil { + test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToPerformRefresh(t, + test.refreshRequest.want.wantUpstreamOIDCRefreshCall.performedByUpstreamName, + test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args, + ) + } else { + test.idps.RequireExactlyZeroCallsToPerformRefresh(t) + } + + // Test that we did or did not make a call to the upstream OIDC provider interface to validate the + // new ID token that was returned by the upstream refresh. + if test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall != nil { + test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToValidateToken(t, + test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall.performedByUpstreamName, + test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall.args, + ) + } else { + test.idps.RequireExactlyZeroCallsToValidateToken(t) + } + // The bug in fosite that prevents at_hash from appearing in the initial ID token does not impact the refreshed ID token wantAtHashClaimInIDToken := true // Refreshed ID tokens do not include the nonce from the original auth request wantNonceValueInIDToken := false - requireTokenEndpointBehavior(t, test.refreshRequest.want, wantAtHashClaimInIDToken, wantNonceValueInIDToken, refreshResponse, authCode, oauthStore, jwtSigningKey, secrets) + + requireTokenEndpointBehavior(t, + test.refreshRequest.want, + test.authcodeExchange.customSessionData, + wantAtHashClaimInIDToken, + wantNonceValueInIDToken, + refreshResponse, + authCode, + oauthStore, + jwtSigningKey, + secrets, + ) if test.refreshRequest.want.wantStatus == http.StatusOK { wantIDToken := contains(test.refreshRequest.want.wantSuccessBodyFields, "id_token") @@ -1109,7 +1622,7 @@ func requireClaimsAreEqual(t *testing.T, claimName string, claimsOfTokenA map[st require.Equal(t, claimsOfTokenA[claimName], claimsOfTokenB[claimName]) } -func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) ( +func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs, idps provider.DynamicUpstreamIDPProvider) ( subject http.Handler, rsp *httptest.ResponseRecorder, authCode string, @@ -1129,15 +1642,17 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) ( oauthStore = oidc.NewKubeStorage(secrets, oidc.DefaultOIDCTimeoutsConfiguration()) if test.makeOathHelper != nil { - oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore) + oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore, test.customSessionData) } else { - oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore) + // Note that makeHappyOauthHelper() calls simulateAuthEndpointHavingAlreadyRun() to preload the session storage. + oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore, test.customSessionData) } if test.modifyStorage != nil { test.modifyStorage(t, oauthStore, authCode) } - subject = NewHandler(oauthHelper) + + subject = NewHandler(idps, oauthHelper) authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid") expectedNumberOfIDSessionsStored := 0 @@ -1163,7 +1678,18 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) ( wantAtHashClaimInIDToken := false // due to a bug in fosite, the at_hash claim is not filled in during authcode exchange wantNonceValueInIDToken := true // ID tokens returned by the authcode exchange must include the nonce from the auth request (unliked refreshed ID tokens) - requireTokenEndpointBehavior(t, test.want, wantAtHashClaimInIDToken, wantNonceValueInIDToken, rsp, authCode, oauthStore, jwtSigningKey, secrets) + + requireTokenEndpointBehavior(t, + test.want, + test.customSessionData, + wantAtHashClaimInIDToken, + wantNonceValueInIDToken, + rsp, + authCode, + oauthStore, + jwtSigningKey, + secrets, + ) return subject, rsp, authCode, jwtSigningKey, secrets, oauthStore } @@ -1171,6 +1697,7 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) ( func requireTokenEndpointBehavior( t *testing.T, test tokenEndpointResponseExpectedValues, + oldCustomSessionData *psession.CustomSessionData, wantAtHashClaimInIDToken bool, wantNonceValueInIDToken bool, tokenEndpointResponse *httptest.ResponseRecorder, @@ -1193,9 +1720,10 @@ func requireTokenEndpointBehavior( wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token") requireInvalidAuthCodeStorage(t, authCode, oauthStore, secrets) - requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, secrets) + requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, test.wantCustomSessionDataStored, secrets) requireInvalidPKCEStorage(t, authCode, oauthStore) - requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes) + // Performing a refresh does not update the OIDC storage, so after a refresh it should still have the old custom session data from the initial login. + requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, oldCustomSessionData) expectedNumberOfRefreshTokenSessionsStored := 0 if wantRefreshToken { @@ -1207,7 +1735,7 @@ func requireTokenEndpointBehavior( requireValidIDToken(t, parsedResponseBody, jwtSigningKey, wantAtHashClaimInIDToken, wantNonceValueInIDToken, parsedResponseBody["access_token"].(string)) } if wantRefreshToken { - requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, secrets) + requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes, test.wantCustomSessionDataStored, secrets) } testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) @@ -1304,16 +1832,24 @@ func getFositeDataSignature(t *testing.T, data string) string { return split[1] } +type OauthHelperFactoryFunc func( + t *testing.T, + authRequest *http.Request, + store fositestoragei.AllFositeStorage, + initialCustomSessionData *psession.CustomSessionData, +) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) + func makeHappyOauthHelper( t *testing.T, authRequest *http.Request, store fositestoragei.AllFositeStorage, + initialCustomSessionData *psession.CustomSessionData, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) - authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) + authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) return oauthHelper, authResponder.GetCode(), jwtSigningKey } @@ -1334,12 +1870,13 @@ func makeOauthHelperWithJWTKeyThatWorksOnlyOnce( t *testing.T, authRequest *http.Request, store fositestoragei.AllFositeStorage, + initialCustomSessionData *psession.CustomSessionData, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() jwtSigningKey, jwkProvider := generateJWTSigningKeyAndJWKSProvider(t, goodIssuer) oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, &singleUseJWKProvider{DynamicJWKSProvider: jwkProvider}, oidc.DefaultOIDCTimeoutsConfiguration()) - authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) + authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) return oauthHelper, authResponder.GetCode(), jwtSigningKey } @@ -1347,17 +1884,23 @@ func makeOauthHelperWithNilPrivateJWTSigningKey( t *testing.T, authRequest *http.Request, store fositestoragei.AllFositeStorage, + initialCustomSessionData *psession.CustomSessionData, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() jwkProvider := jwks.NewDynamicJWKSProvider() // empty provider which contains no signing key for this issuer oauthHelper := oidc.FositeOauth2Helper(store, goodIssuer, hmacSecretFunc, jwkProvider, oidc.DefaultOIDCTimeoutsConfiguration()) - authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper) + authResponder := simulateAuthEndpointHavingAlreadyRun(t, authRequest, oauthHelper, initialCustomSessionData) return oauthHelper, authResponder.GetCode(), nil } // Simulate the auth endpoint running so Fosite code will fill the store with realistic values. -func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider) fosite.AuthorizeResponder { +func simulateAuthEndpointHavingAlreadyRun( + t *testing.T, + authRequest *http.Request, + oauthHelper fosite.OAuth2Provider, + initialCustomSessionData *psession.CustomSessionData, +) fosite.AuthorizeResponder { // We only set the fields in the session that Fosite wants us to set. ctx := context.Background() session := &psession.PinnipedSession{ @@ -1374,11 +1917,7 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques Subject: "", // 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.CustomSessionData{ - OIDC: &psession.OIDCSessionData{ - UpstreamRefreshToken: "starting-fake-refresh-token", - }, - }, + Custom: initialCustomSessionData, } authRequester, err := oauthHelper.NewAuthorizeRequest(ctx, authRequest) require.NoError(t, err) @@ -1416,7 +1955,7 @@ func generateJWTSigningKeyAndJWKSProvider(t *testing.T, issuer string) (*ecdsa.P func requireInvalidAuthCodeStorage( t *testing.T, code string, - storage oauth2.CoreStorage, + storage fositeoauth2.CoreStorage, secrets v1.SecretInterface, ) { t.Helper() @@ -1431,9 +1970,10 @@ func requireInvalidAuthCodeStorage( func requireValidRefreshTokenStorage( t *testing.T, body map[string]interface{}, - storage oauth2.CoreStorage, + storage fositeoauth2.CoreStorage, wantRequestedScopes []string, wantGrantedScopes []string, + wantCustomSessionData *psession.CustomSessionData, secrets v1.SecretInterface, ) { t.Helper() @@ -1455,6 +1995,7 @@ func requireValidRefreshTokenStorage( wantRequestedScopes, wantGrantedScopes, true, + wantCustomSessionData, ) requireGarbageCollectTimeInDelta(t, refreshTokenString, "refresh-token", secrets, time.Now().Add(9*time.Hour).Add(2*time.Minute), 1*time.Minute) @@ -1463,9 +2004,10 @@ func requireValidRefreshTokenStorage( func requireValidAccessTokenStorage( t *testing.T, body map[string]interface{}, - storage oauth2.CoreStorage, + storage fositeoauth2.CoreStorage, wantRequestedScopes []string, wantGrantedScopes []string, + wantCustomSessionData *psession.CustomSessionData, secrets v1.SecretInterface, ) { t.Helper() @@ -1506,6 +2048,7 @@ func requireValidAccessTokenStorage( wantRequestedScopes, wantGrantedScopes, true, + wantCustomSessionData, ) requireGarbageCollectTimeInDelta(t, accessTokenString, "access-token", secrets, time.Now().Add(9*time.Hour).Add(2*time.Minute), 1*time.Minute) @@ -1514,7 +2057,7 @@ func requireValidAccessTokenStorage( func requireInvalidAccessTokenStorage( t *testing.T, body map[string]interface{}, - storage oauth2.CoreStorage, + storage fositeoauth2.CoreStorage, ) { t.Helper() @@ -1547,6 +2090,7 @@ func requireValidOIDCStorage( storage openid.OpenIDConnectRequestStorage, wantRequestedScopes []string, wantGrantedScopes []string, + wantCustomSessionData *psession.CustomSessionData, ) { t.Helper() @@ -1569,6 +2113,7 @@ func requireValidOIDCStorage( wantRequestedScopes, wantGrantedScopes, false, + wantCustomSessionData, ) } else { _, err := storage.GetOpenIDConnectSession(context.Background(), code, nil) @@ -1583,6 +2128,7 @@ func requireValidStoredRequest( wantRequestedScopes []string, wantGrantedScopes []string, wantAccessTokenExpiresAt bool, + wantCustomSessionData *psession.CustomSessionData, ) { t.Helper() @@ -1669,6 +2215,9 @@ func requireValidStoredRequest( // We don't use these, so they should be empty. require.Empty(t, session.Fosite.Username) require.Empty(t, session.Fosite.Subject) + + // The custom session data was stored as expected. + require.Equal(t, wantCustomSessionData, session.Custom) } func requireGarbageCollectTimeInDelta(t *testing.T, tokenString string, typeLabel string, secrets v1.SecretInterface, wantExpirationTime time.Time, deltaTime time.Duration) { diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 3f8c3f57..90361ddd 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -58,6 +58,21 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct { Password string } +// PerformRefreshArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc(). +type PerformRefreshArgs struct { + Ctx context.Context + RefreshToken string +} + +// ValidateTokenArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.ValidateTokenFunc(). +type ValidateTokenArgs struct { + Ctx context.Context + Tok *oauth2.Token + ExpectedIDTokenNonce nonce.Nonce +} + type TestUpstreamLDAPIdentityProvider struct { Name string ResourceUID types.UID @@ -107,10 +122,18 @@ type TestUpstreamOIDCIdentityProvider struct { password string, ) (*oidctypes.Token, error) + PerformRefreshFunc func(ctx context.Context, refreshToken string) (*oauth2.Token, error) + + ValidateTokenFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) + exchangeAuthcodeAndValidateTokensCallCount int exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs passwordCredentialsGrantAndValidateTokensCallCount int passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs + performRefreshCallCount int + performRefreshArgs []*PerformRefreshArgs + validateTokenCallCount int + validateTokenArgs []*ValidateTokenArgs } var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{} @@ -193,8 +216,51 @@ func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs return u.exchangeAuthcodeAndValidateTokensArgs[call] } -func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *oauth2.Token, _ nonce.Nonce) (*oidctypes.Token, error) { - panic("implement me") +func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + if u.performRefreshArgs == nil { + u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + } + u.performRefreshCallCount++ + u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ + Ctx: ctx, + RefreshToken: refreshToken, + }) + return u.PerformRefreshFunc(ctx, refreshToken) +} + +func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshCallCount() int { + return u.performRefreshCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs { + if u.performRefreshArgs == nil { + u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + } + return u.performRefreshArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { + if u.validateTokenArgs == nil { + u.validateTokenArgs = make([]*ValidateTokenArgs, 0) + } + u.validateTokenCallCount++ + u.validateTokenArgs = append(u.validateTokenArgs, &ValidateTokenArgs{ + Ctx: ctx, + Tok: tok, + ExpectedIDTokenNonce: expectedIDTokenNonce, + }) + return u.ValidateTokenFunc(ctx, tok, expectedIDTokenNonce) +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenCallCount() int { + return u.validateTokenCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenArgs(call int) *ValidateTokenArgs { + if u.validateTokenArgs == nil { + u.validateTokenArgs = make([]*ValidateTokenArgs, 0) + } + return u.validateTokenArgs[call] } type UpstreamIDPListerBuilder struct { @@ -316,6 +382,80 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToExchangeAuthcodeAndV ) } +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *PerformRefreshArgs, +) { + t.Helper() + var actualArgs *PerformRefreshArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.performRefreshArgs[0] + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to PerformRefresh() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "PerformRefresh() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.performRefreshCallCount + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to PerformRefresh()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *ValidateTokenArgs, +) { + t.Helper() + var actualArgs *ValidateTokenArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.validateTokenCallCount + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.validateTokenArgs[0] + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to ValidateToken() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "ValidateToken() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToValidateToken(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.validateTokenCallCount + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to ValidateToken()", + ) +} + func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { return &UpstreamIDPListerBuilder{} } @@ -329,11 +469,15 @@ type TestUpstreamOIDCIdentityProviderBuilder struct { refreshToken *oidctypes.RefreshToken usernameClaim string groupsClaim string + refreshedTokens *oauth2.Token + validatedTokens *oidctypes.Token authorizationURL url.URL additionalAuthcodeParams map[string]string allowPasswordGrant bool authcodeExchangeErr error passwordGrantErr error + performRefreshErr error + validateTokenErr error } func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { @@ -429,6 +573,26 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPasswordGrantError(err err return u } +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshedTokens(tokens *oauth2.Token) *TestUpstreamOIDCIdentityProviderBuilder { + u.refreshedTokens = tokens + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPerformRefreshError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.performRefreshErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder { + u.validatedTokens = tokens + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.validateTokenErr = err + return u +} + func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { return &TestUpstreamOIDCIdentityProvider{ Name: u.name, @@ -452,6 +616,18 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent } return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil }, + PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + if u.performRefreshErr != nil { + return nil, u.performRefreshErr + } + return u.refreshedTokens, nil + }, + ValidateTokenFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { + if u.validateTokenErr != nil { + return nil, u.validateTokenErr + } + return u.validatedTokens, nil + }, } } diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index 17e70915..34c27be2 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -47,6 +47,8 @@ type ProviderConfig struct { } } +var _ provider.UpstreamOIDCIdentityProviderI = (*ProviderConfig)(nil) + func (p *ProviderConfig) GetResourceUID() types.UID { return p.ResourceUID } @@ -120,6 +122,14 @@ func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context, return p.ValidateToken(ctx, tok, expectedIDTokenNonce) } +func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + // Create a TokenSource without an access token, so it thinks that a refresh is immediately required. + // Then ask it for the tokens to cause it to perform the refresh and return the results. + return p.Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken}).Token() +} + +// ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response, +// if the provider offers the userinfo endpoint. func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { idTok, hasIDTok := tok.Extra("id_token").(string) if !hasIDTok { @@ -146,7 +156,7 @@ func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, e } maybeLogClaims("claims from ID token", p.Name, validatedClaims) - if err := p.fetchUserInfo(ctx, tok, validatedClaims); err != nil { + if err := p.maybeFetchUserInfoAndMergeClaims(ctx, tok, validatedClaims); err != nil { return nil, httperr.Wrap(http.StatusInternalServerError, "could not fetch user info claims", err) } @@ -167,7 +177,7 @@ func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, e }, nil } -func (p *ProviderConfig) fetchUserInfo(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}) error { +func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}) error { idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string) if len(idTokenSubject) == 0 { return nil // defer to existing ID token validation diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index 2ffd40ca..342683fc 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/mocks/mockkeyset" + "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" ) @@ -288,6 +289,171 @@ func TestProviderConfig(t *testing.T) { } }) + t.Run("PerformRefresh", func(t *testing.T) { + tests := []struct { + name string + returnIDTok string + returnAccessTok string + returnRefreshTok string + returnTokType string + returnExpiresIn string + tokenStatusCode int + + wantErr string + wantToken *oauth2.Token + wantTokenExtras map[string]interface{} + }{ + { + name: "success when the server returns all tokens in the refresh result", + returnIDTok: "test-id-token", + returnAccessTok: "test-access-token", + returnRefreshTok: "test-refresh-token", + returnTokType: "test-token-type", + returnExpiresIn: "42", + tokenStatusCode: http.StatusOK, + wantToken: &oauth2.Token{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + TokenType: "test-token-type", + Expiry: time.Now().Add(42 * time.Second), + }, + wantTokenExtras: map[string]interface{}{ + // the ID token only appears in the extras map + "id_token": "test-id-token", + // the library also repeats all the other keys/values returned by the server in the raw extras map + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "test-token-type", + "expires_in": "42", + // the library also adds this zero-value even though the server did not return it + "expiry": "0001-01-01T00:00:00Z", + }, + }, + { + name: "success when the server does not return a new refresh token in the refresh result", + returnIDTok: "test-id-token", + returnAccessTok: "test-access-token", + returnRefreshTok: "", + returnTokType: "test-token-type", + returnExpiresIn: "42", + tokenStatusCode: http.StatusOK, + wantToken: &oauth2.Token{ + AccessToken: "test-access-token", + // the library sets the original refresh token into the result, even though the server did not return that + RefreshToken: "test-initial-refresh-token", + TokenType: "test-token-type", + Expiry: time.Now().Add(42 * time.Second), + }, + wantTokenExtras: map[string]interface{}{ + // the ID token only appears in the extras map + "id_token": "test-id-token", + // the library also repeats all the other keys/values returned by the server in the raw extras map + "access_token": "test-access-token", + "token_type": "test-token-type", + "expires_in": "42", + // the library also adds this zero-value even though the server did not return it + "expiry": "0001-01-01T00:00:00Z", + }, + }, + { + name: "success when the server does not return a new ID token in the refresh result", + returnIDTok: "", + returnAccessTok: "test-access-token", + returnRefreshTok: "test-refresh-token", + returnTokType: "test-token-type", + returnExpiresIn: "42", + tokenStatusCode: http.StatusOK, + wantToken: &oauth2.Token{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + TokenType: "test-token-type", + Expiry: time.Now().Add(42 * time.Second), + }, + wantTokenExtras: map[string]interface{}{ + // the library also repeats all the other keys/values returned by the server in the raw extras map + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "test-token-type", + "expires_in": "42", + // the library also adds this zero-value even though the server did not return it + "expiry": "0001-01-01T00:00:00Z", + }, + }, + { + name: "server returns an error on token refresh", + tokenStatusCode: http.StatusForbidden, + wantErr: "oauth2: cannot fetch token: 403 Forbidden\nResponse: fake error\n", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.NoError(t, r.ParseForm()) + require.Equal(t, 4, len(r.Form)) + require.Equal(t, "test-client-id", r.Form.Get("client_id")) + require.Equal(t, "test-client-secret", r.Form.Get("client_secret")) + require.Equal(t, "refresh_token", r.Form.Get("grant_type")) + require.Equal(t, "test-initial-refresh-token", r.Form.Get("refresh_token")) + if tt.tokenStatusCode != http.StatusOK { + http.Error(w, "fake error", tt.tokenStatusCode) + return + } + var response struct { + oauth2.Token + IDToken string `json:"id_token,omitempty"` + ExpiresIn string `json:"expires_in,omitempty"` + } + response.IDToken = tt.returnIDTok + response.AccessToken = tt.returnAccessTok + response.RefreshToken = tt.returnRefreshTok + response.TokenType = tt.returnTokType + response.ExpiresIn = tt.returnExpiresIn + w.Header().Set("content-type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(&response)) + })) + t.Cleanup(tokenServer.Close) + + p := ProviderConfig{ + Name: "test-name", + UsernameClaim: "test-username-claim", + GroupsClaim: "test-groups-claim", + Config: &oauth2.Config{ + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://example.com", + TokenURL: tokenServer.URL, + AuthStyle: oauth2.AuthStyleInParams, + }, + Scopes: []string{"scope1", "scope2"}, + }, + } + + tok, err := p.PerformRefresh( + context.Background(), + "test-initial-refresh-token", + ) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, tok) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantToken.TokenType, tok.TokenType) + require.Equal(t, tt.wantToken.RefreshToken, tok.RefreshToken) + require.Equal(t, tt.wantToken.AccessToken, tok.AccessToken) + testutil.RequireTimeInDelta(t, tt.wantToken.Expiry, tok.Expiry, 5*time.Second) + for k, v := range tt.wantTokenExtras { + require.Equal(t, v, tok.Extra(k)) + } + }) + } + }) + t.Run("ExchangeAuthcodeAndValidateTokens", func(t *testing.T) { tests := []struct { name string diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 044417ec..3d62e05d 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -808,9 +808,9 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) { h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.") - refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token}) + upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient) - refreshed, err := refreshSource.Token() + refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) if err != nil { // Ignore errors during refresh, but return nil which will trigger the full login flow. return nil, nil @@ -818,7 +818,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype // The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least // some providers do not include one, so we skip the nonce validation here (but not other validations). - return h.getProvider(h.oauth2Config, h.provider, h.httpClient).ValidateToken(ctx, refreshed, "") + return upstreamOIDCIdentityProvider.ValidateToken(ctx, refreshed, "") } func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) { diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index c63173df..2c30ffb0 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -35,6 +35,7 @@ import ( "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" + "go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -404,11 +405,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo clientID: "test-client-id", opt: func(t *testing.T) Option { return func(h *handlerState) error { - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). Return(&testToken, nil) + mock.EXPECT(). + PerformRefresh(gomock.Any(), testToken.RefreshToken.Token). + DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + // Call the real production code to perform a refresh. + return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken) + }) return mock } @@ -445,11 +452,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo clientID: "test-client-id", opt: func(t *testing.T) Option { return func(h *handlerState) error { - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). Return(nil, fmt.Errorf("some validation error")) + mock.EXPECT(). + PerformRefresh(gomock.Any(), "test-refresh-token-returning-invalid-id-token"). + DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + // Call the real production code to perform a refresh. + return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken) + }) return mock } @@ -1522,11 +1535,17 @@ func TestLogin(t *testing.T) { // nolint:gocyclo }) h.cache = cache - h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI { mock := mockUpstream(t) mock.EXPECT(). ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")). Return(&testToken, nil) + mock.EXPECT(). + PerformRefresh(gomock.Any(), testToken.RefreshToken.Token). + DoAndReturn(func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + // Call the real production code to perform a refresh. + return upstreamoidc.New(config, provider, client).PerformRefresh(ctx, refreshToken) + }) return mock } From a34dae549b345986768ccf2646389e81e590a3a6 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 13 Oct 2021 14:05:00 -0700 Subject: [PATCH 05/19] When performing an upstream refresh, use the configured http client Otherwise, the CA and proxy settings will not be used for the call to the upstream token endpoint while performing the refresh. This mistake was exposed by the TestSupervisorLogin integration test, so it has test coverage. --- internal/upstreamoidc/upstreamoidc.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index 34c27be2..7d4006c8 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -123,9 +123,11 @@ func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context, } func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + // Use the provided HTTP client to benefit from its CA, proxy, and other settings. + httpClientContext := coreosoidc.ClientContext(ctx, p.Client) // Create a TokenSource without an access token, so it thinks that a refresh is immediately required. // Then ask it for the tokens to cause it to perform the refresh and return the results. - return p.Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken}).Token() + return p.Config.TokenSource(httpClientContext, &oauth2.Token{RefreshToken: refreshToken}).Token() } // ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response, From 9e05d175a7f95953d4af6816d3d6659f128fba5e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 13 Oct 2021 15:12:19 -0700 Subject: [PATCH 06/19] Add integration test: upstream refresh failure during downstream refresh --- test/integration/supervisor_login_test.go | 91 +++++++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index b79d5255..770d67b7 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -29,6 +29,7 @@ import ( idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -50,6 +51,11 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenGroups []string wantErrorDescription string wantErrorType string + + // We don't necessarily have any way to revoke the user's session on the upstream provider, + // so to cause the upstream refresh to fail we can cheat by manipulating the user's session + // data in such a way that it should cause the next upstream refresh attempt to fail. + breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData) }{ { name: "oidc with default username and groups claim settings", @@ -69,6 +75,11 @@ func TestSupervisorLogin(t *testing.T) { }, idpv1alpha1.PhaseReady) }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { + require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) + customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" + }, // the ID token Subject should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", // the ID token Username should include the upstream user ID after the upstream issuer name @@ -98,7 +109,12 @@ func TestSupervisorLogin(t *testing.T) { }, }, idpv1alpha1.PhaseReady) }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { + require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) + customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" + }, wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, @@ -132,6 +148,11 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { + require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) + customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" + }, // the ID token Subject should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", // the ID token Username should include the upstream user ID after the upstream issuer name @@ -193,6 +214,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamLDAP.Host+ @@ -259,6 +281,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ @@ -325,8 +348,9 @@ func TestSupervisorLogin(t *testing.T) { true, ) }, - wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", - wantErrorType: "access_denied", + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type + wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", + wantErrorType: "access_denied", }, { name: "ldap login still works after updating bind secret", @@ -402,6 +426,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamLDAP.Host+ @@ -500,6 +525,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamLDAP.Host+ @@ -554,6 +580,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ @@ -621,6 +648,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ @@ -693,6 +721,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ @@ -780,6 +809,7 @@ func TestSupervisorLogin(t *testing.T) { false, ) }, + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ @@ -834,8 +864,9 @@ func TestSupervisorLogin(t *testing.T) { true, ) }, - wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", - wantErrorType: "access_denied", + breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type + wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", + wantErrorType: "access_denied", }, } for _, test := range tests { @@ -846,10 +877,12 @@ func TestSupervisorLogin(t *testing.T) { testSupervisorLogin(t, tt.createIDP, tt.requestAuthorization, + tt.breakRefreshSessionData, tt.wantDownstreamIDTokenSubjectToMatch, tt.wantDownstreamIDTokenUsernameToMatch, tt.wantDownstreamIDTokenGroups, - tt.wantErrorDescription, tt.wantErrorType, + tt.wantErrorDescription, + tt.wantErrorType, ) }) } @@ -976,6 +1009,7 @@ func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T), requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), + breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData), wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, wantErrorDescription string, wantErrorType string, ) { @@ -1140,6 +1174,42 @@ func testSupervisorLogin( // token exchange on the refreshed token doTokenExchange(t, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery) + + // Now that we have successfully performed a refresh, let's test what happens when an + // upstream refresh fails during the next downstream refresh. + if breakRefreshSessionData != nil { + latestRefreshToken := refreshedTokenResponse.RefreshToken + signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken) + + // First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage. + kubeClient := testlib.NewKubernetesClientset(t) + supervisorSecretsClient := kubeClient.CoreV1().Secrets(env.SupervisorNamespace) + oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, oidc.DefaultOIDCTimeoutsConfiguration()) + storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil) + require.NoError(t, err) + + // Next mutate the part of the session that is used during upstream refresh. + pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession) + require.True(t, ok, "should have been able to cast session data to PinnipedSession") + breakRefreshSessionData(t, pinnipedSession.Custom) + + // Then save the mutated Secret back to Kubernetes. + // There is no update function, so delete and create again at the same name. + require.NoError(t, oauthStore.DeleteRefreshTokenSession(ctx, signatureOfLatestRefreshToken)) + require.NoError(t, oauthStore.CreateRefreshTokenSession(ctx, signatureOfLatestRefreshToken, storedRefreshSession)) + + // Now try to perform a downstream refresh again, knowing that the corresponding upstream refresh should fail. + _, err = downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: latestRefreshToken}).Token() + // Should have got an error since the upstream refresh should have failed. + require.Error(t, err) + require.Regexp(t, + regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+ + regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed using provider '`)+ + "[^']+"+ // this would be the name of the identity provider CR + regexp.QuoteMeta(fmt.Sprintf(`' of type '%s'."`, pinnipedSession.Custom.ProviderType)), + err.Error(), + ) + } } else { errorDescription := callback.URL.Query().Get("error_description") errorType := callback.URL.Query().Get("error") @@ -1148,6 +1218,15 @@ func testSupervisorLogin( } } +// getFositeDataSignature returns the signature of the provided data. The provided data could be an auth code, access +// token, etc. It is assumed that the code is of the format "data.signature", which is how Fosite generates auth codes +// and access tokens. +func getFositeDataSignature(t *testing.T, data string) string { + split := strings.Split(data, ".") + require.Len(t, split, 2) + return split[1] +} + func verifyTokenResponse( t *testing.T, tokenResponse *oauth2.Token, From ddb23bd2edc17d79f657759b3057d76a61a0915c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 14 Oct 2021 15:49:44 -0700 Subject: [PATCH 07/19] Add upstream refresh related config to OIDCIdentityProvider CRD Also update related docs. --- .../types_oidcidentityprovider.go.tmpl | 83 ++++++-- ...or.pinniped.dev_oidcidentityproviders.yaml | 111 +++++++++- generated/1.17/README.adoc | 30 ++- .../v1alpha1/types_oidcidentityprovider.go | 83 ++++++-- .../idp/v1alpha1/zz_generated.deepcopy.go | 21 ++ ...or.pinniped.dev_oidcidentityproviders.yaml | 111 +++++++++- generated/1.18/README.adoc | 30 ++- .../v1alpha1/types_oidcidentityprovider.go | 83 ++++++-- .../idp/v1alpha1/zz_generated.deepcopy.go | 21 ++ ...or.pinniped.dev_oidcidentityproviders.yaml | 111 +++++++++- generated/1.19/README.adoc | 30 ++- .../v1alpha1/types_oidcidentityprovider.go | 83 ++++++-- .../idp/v1alpha1/zz_generated.deepcopy.go | 21 ++ ...or.pinniped.dev_oidcidentityproviders.yaml | 111 +++++++++- generated/1.20/README.adoc | 30 ++- .../v1alpha1/types_oidcidentityprovider.go | 83 ++++++-- .../idp/v1alpha1/zz_generated.deepcopy.go | 21 ++ ...or.pinniped.dev_oidcidentityproviders.yaml | 111 +++++++++- .../v1alpha1/types_oidcidentityprovider.go | 83 ++++++-- .../idp/v1alpha1/zz_generated.deepcopy.go | 21 ++ go.mod | 4 +- .../oidc_upstream_watcher.go | 64 +++++- .../oidc_upstream_watcher_test.go | 196 +++++++++++------- .../howto/configure-supervisor-with-dex.md | 14 +- .../howto/configure-supervisor-with-gitlab.md | 23 +- .../howto/configure-supervisor-with-okta.md | 33 ++- 26 files changed, 1364 insertions(+), 248 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl index 5d7277bf..9e0624a0 100644 --- a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 70d3865d..390f4848 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -57,13 +57,22 @@ spec: OIDC identity provider. properties: additionalScopes: - description: AdditionalScopes are the scopes in addition to "openid" - that will be requested as part of the authorization request - flow with an OIDC identity provider. In the case of a Resource - Owner Password Credentials Grant flow, AdditionalScopes are - the scopes in addition to "openid" that will be requested as - part of the token request (see also the allowPasswordGrant field). - By default, only the "openid" scope will be requested. + description: AdditionalScopes are the additional scopes that will + be requested from your OIDC provider in the authorization request + during an OIDC Authorization Code Flow and in the token request + during a Resource Owner Password Credentials Grant. Note that + the "openid" scope will always be requested regardless of the + value in this setting, since it is always required according + to the OIDC spec. The "offline_access" scope may also be included + according to the value of the DoNotRequestOfflineAccess setting. + Any other scopes required should be included here in the AdditionalScopes + list. For example, you might like to include scopes like "profile", + "email", or "groups" in order to receive the related claims + in the returned ID token or userinfo endpoint results if you + would like to make use of those claims in the OIDCClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider's documentation for more information + about what scopes are available to request claims. items: type: string type: array @@ -97,18 +106,98 @@ spec: during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. type: boolean + doNotRequestOfflineAccess: + description: DoNotRequestOfflineAccess determines if the "offline_access" + scope will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant in + order to ask to receive a refresh token in the response. Starting + in v0.13.0, the Pinniped Supervisor requires that your OIDC + provider returns refresh tokens to the Supervisor from these + authorization flows. For most OIDC providers, the scope required + to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. See the documentation + of your OIDC provider's authorization and token endpoints for + its requirements for what to include in the request in order + to receive a refresh token in the response, if anything. By + default, DoNotRequestOfflineAccess is false, which means that + "offline_access" will be sent in the authorization request, + since that is what is suggested by the OIDC specification. Note + that it may be safe to send "offline_access" even to providers + which do not require it, since the provider may ignore scopes + that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, set DoNotRequestOfflineAccess to true. This is required + if your OIDC provider will reject the request when it includes + "offline_access" (e.g. GitLab's OIDC provider). If you need + to send some other scope to request a refresh token, include + the scope name in the additionalScopes setting. Also note that + some OIDC providers may require that the "prompt" param be set + to a specific value for the authorization request during an + OIDC Authorization Code Flow in order to receive a refresh token + in the response. To adjust the prompt param, see the additionalAuthorizeParameters + setting. + type: boolean + extraAuthorizeParameters: + description: AdditionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. This setting does not influence the parameters sent + to the token endpoint in the Resource Owner Password Credentials + Grant. Starting in v0.13.0, the Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the doNotRequestOfflineAccess + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. properties: groups: - description: Groups provides the name of the token claim that - will be used to ascertain the groups to which an identity belongs. + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. type: string username: - description: Username provides the name of the token claim that - will be used to ascertain an identity's username. + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. type: string type: object client: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 479b7026..e3c28fbf 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -1099,7 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. +| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. +| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. +| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. |=== @@ -1117,8 +1119,8 @@ OIDCClaims provides a mapping from upstream claims into identities. [cols="25a,75a", options="header"] |=== | Field | Description -| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. -| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. +| *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured. +| *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token. |=== @@ -1164,7 +1166,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] ==== OIDCIdentityProviderSpec -Spec for configuring an OIDC identity provider. +OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. .Appears In: **** @@ -1185,7 +1187,7 @@ Spec for configuring an OIDC identity provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] ==== OIDCIdentityProviderStatus -Status of an OIDC identity provider. +OIDCIdentityProviderStatus is the status of an OIDC identity provider. .Appears In: **** @@ -1200,6 +1202,24 @@ Status of an OIDC identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter"] +==== Parameter + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`name`* __string__ | The name of the parameter. Required. +| *`value`* __string__ | The value of the parameter. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 5d7277bf..9e0624a0 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 9895a76e..12e67583 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } return } @@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 70d3865d..390f4848 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -57,13 +57,22 @@ spec: OIDC identity provider. properties: additionalScopes: - description: AdditionalScopes are the scopes in addition to "openid" - that will be requested as part of the authorization request - flow with an OIDC identity provider. In the case of a Resource - Owner Password Credentials Grant flow, AdditionalScopes are - the scopes in addition to "openid" that will be requested as - part of the token request (see also the allowPasswordGrant field). - By default, only the "openid" scope will be requested. + description: AdditionalScopes are the additional scopes that will + be requested from your OIDC provider in the authorization request + during an OIDC Authorization Code Flow and in the token request + during a Resource Owner Password Credentials Grant. Note that + the "openid" scope will always be requested regardless of the + value in this setting, since it is always required according + to the OIDC spec. The "offline_access" scope may also be included + according to the value of the DoNotRequestOfflineAccess setting. + Any other scopes required should be included here in the AdditionalScopes + list. For example, you might like to include scopes like "profile", + "email", or "groups" in order to receive the related claims + in the returned ID token or userinfo endpoint results if you + would like to make use of those claims in the OIDCClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider's documentation for more information + about what scopes are available to request claims. items: type: string type: array @@ -97,18 +106,98 @@ spec: during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. type: boolean + doNotRequestOfflineAccess: + description: DoNotRequestOfflineAccess determines if the "offline_access" + scope will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant in + order to ask to receive a refresh token in the response. Starting + in v0.13.0, the Pinniped Supervisor requires that your OIDC + provider returns refresh tokens to the Supervisor from these + authorization flows. For most OIDC providers, the scope required + to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. See the documentation + of your OIDC provider's authorization and token endpoints for + its requirements for what to include in the request in order + to receive a refresh token in the response, if anything. By + default, DoNotRequestOfflineAccess is false, which means that + "offline_access" will be sent in the authorization request, + since that is what is suggested by the OIDC specification. Note + that it may be safe to send "offline_access" even to providers + which do not require it, since the provider may ignore scopes + that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, set DoNotRequestOfflineAccess to true. This is required + if your OIDC provider will reject the request when it includes + "offline_access" (e.g. GitLab's OIDC provider). If you need + to send some other scope to request a refresh token, include + the scope name in the additionalScopes setting. Also note that + some OIDC providers may require that the "prompt" param be set + to a specific value for the authorization request during an + OIDC Authorization Code Flow in order to receive a refresh token + in the response. To adjust the prompt param, see the additionalAuthorizeParameters + setting. + type: boolean + extraAuthorizeParameters: + description: AdditionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. This setting does not influence the parameters sent + to the token endpoint in the Resource Owner Password Credentials + Grant. Starting in v0.13.0, the Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the doNotRequestOfflineAccess + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. properties: groups: - description: Groups provides the name of the token claim that - will be used to ascertain the groups to which an identity belongs. + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. type: string username: - description: Username provides the name of the token claim that - will be used to ascertain an identity's username. + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. type: string type: object client: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 1e0f5b16..c08b2d5e 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -1099,7 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. +| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. +| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. +| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. |=== @@ -1117,8 +1119,8 @@ OIDCClaims provides a mapping from upstream claims into identities. [cols="25a,75a", options="header"] |=== | Field | Description -| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. -| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. +| *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured. +| *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token. |=== @@ -1164,7 +1166,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] ==== OIDCIdentityProviderSpec -Spec for configuring an OIDC identity provider. +OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. .Appears In: **** @@ -1185,7 +1187,7 @@ Spec for configuring an OIDC identity provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] ==== OIDCIdentityProviderStatus -Status of an OIDC identity provider. +OIDCIdentityProviderStatus is the status of an OIDC identity provider. .Appears In: **** @@ -1200,6 +1202,24 @@ Status of an OIDC identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter"] +==== Parameter + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`name`* __string__ | The name of the parameter. Required. +| *`value`* __string__ | The value of the parameter. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 5d7277bf..9e0624a0 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 9895a76e..12e67583 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } return } @@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 70d3865d..390f4848 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -57,13 +57,22 @@ spec: OIDC identity provider. properties: additionalScopes: - description: AdditionalScopes are the scopes in addition to "openid" - that will be requested as part of the authorization request - flow with an OIDC identity provider. In the case of a Resource - Owner Password Credentials Grant flow, AdditionalScopes are - the scopes in addition to "openid" that will be requested as - part of the token request (see also the allowPasswordGrant field). - By default, only the "openid" scope will be requested. + description: AdditionalScopes are the additional scopes that will + be requested from your OIDC provider in the authorization request + during an OIDC Authorization Code Flow and in the token request + during a Resource Owner Password Credentials Grant. Note that + the "openid" scope will always be requested regardless of the + value in this setting, since it is always required according + to the OIDC spec. The "offline_access" scope may also be included + according to the value of the DoNotRequestOfflineAccess setting. + Any other scopes required should be included here in the AdditionalScopes + list. For example, you might like to include scopes like "profile", + "email", or "groups" in order to receive the related claims + in the returned ID token or userinfo endpoint results if you + would like to make use of those claims in the OIDCClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider's documentation for more information + about what scopes are available to request claims. items: type: string type: array @@ -97,18 +106,98 @@ spec: during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. type: boolean + doNotRequestOfflineAccess: + description: DoNotRequestOfflineAccess determines if the "offline_access" + scope will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant in + order to ask to receive a refresh token in the response. Starting + in v0.13.0, the Pinniped Supervisor requires that your OIDC + provider returns refresh tokens to the Supervisor from these + authorization flows. For most OIDC providers, the scope required + to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. See the documentation + of your OIDC provider's authorization and token endpoints for + its requirements for what to include in the request in order + to receive a refresh token in the response, if anything. By + default, DoNotRequestOfflineAccess is false, which means that + "offline_access" will be sent in the authorization request, + since that is what is suggested by the OIDC specification. Note + that it may be safe to send "offline_access" even to providers + which do not require it, since the provider may ignore scopes + that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, set DoNotRequestOfflineAccess to true. This is required + if your OIDC provider will reject the request when it includes + "offline_access" (e.g. GitLab's OIDC provider). If you need + to send some other scope to request a refresh token, include + the scope name in the additionalScopes setting. Also note that + some OIDC providers may require that the "prompt" param be set + to a specific value for the authorization request during an + OIDC Authorization Code Flow in order to receive a refresh token + in the response. To adjust the prompt param, see the additionalAuthorizeParameters + setting. + type: boolean + extraAuthorizeParameters: + description: AdditionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. This setting does not influence the parameters sent + to the token endpoint in the Resource Owner Password Credentials + Grant. Starting in v0.13.0, the Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the doNotRequestOfflineAccess + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. properties: groups: - description: Groups provides the name of the token claim that - will be used to ascertain the groups to which an identity belongs. + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. type: string username: - description: Username provides the name of the token claim that - will be used to ascertain an identity's username. + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. type: string type: object client: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index f1bae9ef..f3d0aa51 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -1099,7 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. +| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. +| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. +| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. |=== @@ -1117,8 +1119,8 @@ OIDCClaims provides a mapping from upstream claims into identities. [cols="25a,75a", options="header"] |=== | Field | Description -| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. -| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. +| *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured. +| *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token. |=== @@ -1164,7 +1166,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] ==== OIDCIdentityProviderSpec -Spec for configuring an OIDC identity provider. +OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. .Appears In: **** @@ -1185,7 +1187,7 @@ Spec for configuring an OIDC identity provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] ==== OIDCIdentityProviderStatus -Status of an OIDC identity provider. +OIDCIdentityProviderStatus is the status of an OIDC identity provider. .Appears In: **** @@ -1200,6 +1202,24 @@ Status of an OIDC identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter"] +==== Parameter + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`name`* __string__ | The name of the parameter. Required. +| *`value`* __string__ | The value of the parameter. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 5d7277bf..9e0624a0 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 9895a76e..12e67583 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } return } @@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 70d3865d..390f4848 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -57,13 +57,22 @@ spec: OIDC identity provider. properties: additionalScopes: - description: AdditionalScopes are the scopes in addition to "openid" - that will be requested as part of the authorization request - flow with an OIDC identity provider. In the case of a Resource - Owner Password Credentials Grant flow, AdditionalScopes are - the scopes in addition to "openid" that will be requested as - part of the token request (see also the allowPasswordGrant field). - By default, only the "openid" scope will be requested. + description: AdditionalScopes are the additional scopes that will + be requested from your OIDC provider in the authorization request + during an OIDC Authorization Code Flow and in the token request + during a Resource Owner Password Credentials Grant. Note that + the "openid" scope will always be requested regardless of the + value in this setting, since it is always required according + to the OIDC spec. The "offline_access" scope may also be included + according to the value of the DoNotRequestOfflineAccess setting. + Any other scopes required should be included here in the AdditionalScopes + list. For example, you might like to include scopes like "profile", + "email", or "groups" in order to receive the related claims + in the returned ID token or userinfo endpoint results if you + would like to make use of those claims in the OIDCClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider's documentation for more information + about what scopes are available to request claims. items: type: string type: array @@ -97,18 +106,98 @@ spec: during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. type: boolean + doNotRequestOfflineAccess: + description: DoNotRequestOfflineAccess determines if the "offline_access" + scope will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant in + order to ask to receive a refresh token in the response. Starting + in v0.13.0, the Pinniped Supervisor requires that your OIDC + provider returns refresh tokens to the Supervisor from these + authorization flows. For most OIDC providers, the scope required + to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. See the documentation + of your OIDC provider's authorization and token endpoints for + its requirements for what to include in the request in order + to receive a refresh token in the response, if anything. By + default, DoNotRequestOfflineAccess is false, which means that + "offline_access" will be sent in the authorization request, + since that is what is suggested by the OIDC specification. Note + that it may be safe to send "offline_access" even to providers + which do not require it, since the provider may ignore scopes + that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, set DoNotRequestOfflineAccess to true. This is required + if your OIDC provider will reject the request when it includes + "offline_access" (e.g. GitLab's OIDC provider). If you need + to send some other scope to request a refresh token, include + the scope name in the additionalScopes setting. Also note that + some OIDC providers may require that the "prompt" param be set + to a specific value for the authorization request during an + OIDC Authorization Code Flow in order to receive a refresh token + in the response. To adjust the prompt param, see the additionalAuthorizeParameters + setting. + type: boolean + extraAuthorizeParameters: + description: AdditionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. This setting does not influence the parameters sent + to the token endpoint in the Resource Owner Password Credentials + Grant. Starting in v0.13.0, the Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the doNotRequestOfflineAccess + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. properties: groups: - description: Groups provides the name of the token claim that - will be used to ascertain the groups to which an identity belongs. + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. type: string username: - description: Username provides the name of the token claim that - will be used to ascertain an identity's username. + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. type: string type: object client: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index efcde7ab..b78ae013 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -1099,7 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). By default, only the "openid" scope will be requested. +| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. +| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. +| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. |=== @@ -1117,8 +1119,8 @@ OIDCClaims provides a mapping from upstream claims into identities. [cols="25a,75a", options="header"] |=== | Field | Description -| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs. -| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username. +| *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured. +| *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token. |=== @@ -1164,7 +1166,7 @@ OIDCIdentityProvider describes the configuration of an upstream OpenID Connect i [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] ==== OIDCIdentityProviderSpec -Spec for configuring an OIDC identity provider. +OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. .Appears In: **** @@ -1185,7 +1187,7 @@ Spec for configuring an OIDC identity provider. [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] ==== OIDCIdentityProviderStatus -Status of an OIDC identity provider. +OIDCIdentityProviderStatus is the status of an OIDC identity provider. .Appears In: **** @@ -1200,6 +1202,24 @@ Status of an OIDC identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter"] +==== Parameter + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`name`* __string__ | The name of the parameter. Required. +| *`value`* __string__ | The value of the parameter. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 5d7277bf..9e0624a0 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 9895a76e..12e67583 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } return } @@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 70d3865d..390f4848 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -57,13 +57,22 @@ spec: OIDC identity provider. properties: additionalScopes: - description: AdditionalScopes are the scopes in addition to "openid" - that will be requested as part of the authorization request - flow with an OIDC identity provider. In the case of a Resource - Owner Password Credentials Grant flow, AdditionalScopes are - the scopes in addition to "openid" that will be requested as - part of the token request (see also the allowPasswordGrant field). - By default, only the "openid" scope will be requested. + description: AdditionalScopes are the additional scopes that will + be requested from your OIDC provider in the authorization request + during an OIDC Authorization Code Flow and in the token request + during a Resource Owner Password Credentials Grant. Note that + the "openid" scope will always be requested regardless of the + value in this setting, since it is always required according + to the OIDC spec. The "offline_access" scope may also be included + according to the value of the DoNotRequestOfflineAccess setting. + Any other scopes required should be included here in the AdditionalScopes + list. For example, you might like to include scopes like "profile", + "email", or "groups" in order to receive the related claims + in the returned ID token or userinfo endpoint results if you + would like to make use of those claims in the OIDCClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider's documentation for more information + about what scopes are available to request claims. items: type: string type: array @@ -97,18 +106,98 @@ spec: during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. type: boolean + doNotRequestOfflineAccess: + description: DoNotRequestOfflineAccess determines if the "offline_access" + scope will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant in + order to ask to receive a refresh token in the response. Starting + in v0.13.0, the Pinniped Supervisor requires that your OIDC + provider returns refresh tokens to the Supervisor from these + authorization flows. For most OIDC providers, the scope required + to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. See the documentation + of your OIDC provider's authorization and token endpoints for + its requirements for what to include in the request in order + to receive a refresh token in the response, if anything. By + default, DoNotRequestOfflineAccess is false, which means that + "offline_access" will be sent in the authorization request, + since that is what is suggested by the OIDC specification. Note + that it may be safe to send "offline_access" even to providers + which do not require it, since the provider may ignore scopes + that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, set DoNotRequestOfflineAccess to true. This is required + if your OIDC provider will reject the request when it includes + "offline_access" (e.g. GitLab's OIDC provider). If you need + to send some other scope to request a refresh token, include + the scope name in the additionalScopes setting. Also note that + some OIDC providers may require that the "prompt" param be set + to a specific value for the authorization request during an + OIDC Authorization Code Flow in order to receive a refresh token + in the response. To adjust the prompt param, see the additionalAuthorizeParameters + setting. + type: boolean + extraAuthorizeParameters: + description: AdditionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. This setting does not influence the parameters sent + to the token endpoint in the Resource Owner Password Credentials + Grant. Starting in v0.13.0, the Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the doNotRequestOfflineAccess + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. properties: groups: - description: Groups provides the name of the token claim that - will be used to ascertain the groups to which an identity belongs. + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. type: string username: - description: Username provides the name of the token claim that - will be used to ascertain an identity's username. + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. type: string type: object client: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 5d7277bf..9e0624a0 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -20,7 +20,7 @@ const ( PhaseError OIDCIdentityProviderPhase = "Error" ) -// Status of an OIDC identity provider. +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. type OIDCIdentityProviderStatus struct { // Phase summarizes the overall status of the OIDCIdentityProvider. // +kubebuilder:default=Pending @@ -38,14 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization - // request flow with an OIDC identity provider. - // In the case of a Resource Owner Password Credentials Grant flow, AdditionalScopes are the scopes - // in addition to "openid" that will be requested as part of the token request (see also the allowPasswordGrant field). - // By default, only the "openid" scope will be requested. + // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in + // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner + // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the + // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these + // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". + // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" + // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what + // to include in the request in order to receive a refresh token in the response, if anything. By default, + // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, + // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to + // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject + // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope + // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC + // providers may require that the "prompt" param be set to a specific value for the authorization request during an + // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see + // the additionalAuthorizeParameters setting. + // +optional + DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` + + // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value + // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes + // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the + // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those + // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` + // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. This setting does not influence the parameters sent to the token endpoint in the + // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC + // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require + // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of + // your OIDC provider's authorization endpoint for its requirements for what to include in the request in + // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to + // request a refresh token, then include it here. Also note that most providers also require a certain scope to be + // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about + // using scopes to request refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. @@ -66,15 +111,29 @@ type OIDCAuthorizationConfig struct { AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + // OIDCClaims provides a mapping from upstream claims into identities. type OIDCClaims struct { - // Groups provides the name of the token claim that will be used to ascertain the groups to which - // an identity belongs. + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. // +optional Groups string `json:"groups"` - // Username provides the name of the token claim that will be used to ascertain an identity's - // username. + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. // +optional Username string `json:"username"` } @@ -89,7 +148,7 @@ type OIDCClient struct { SecretName string `json:"secretName"` } -// Spec for configuring an OIDC identity provider. +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. type OIDCIdentityProviderSpec struct { // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch // /.well-known/openid-configuration. @@ -135,7 +194,7 @@ type OIDCIdentityProvider struct { Status OIDCIdentityProviderStatus `json:"status,omitempty"` } -// List of OIDCIdentityProvider objects. +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type OIDCIdentityProviderList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 9895a76e..12e67583 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -416,6 +416,11 @@ func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } return } @@ -569,6 +574,22 @@ func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { *out = *in diff --git a/go.mod b/go.mod index 1593d1f2..b5f0e8ea 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,9 @@ require ( github.com/google/uuid v1.1.2 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/websocket v1.4.2 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/ory/fosite v0.40.2 + github.com/ory/x v0.0.212 github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/pkg/errors v0.9.1 github.com/sclevine/agouti v3.0.0+incompatible @@ -96,12 +98,10 @@ require ( github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ory/go-acc v0.2.6 // indirect github.com/ory/go-convenience v0.1.0 // indirect github.com/ory/viper v1.7.5 // indirect - github.com/ory/x v0.0.212 // indirect github.com/pborman/uuid v1.2.0 // indirect github.com/pelletier/go-toml v1.9.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 31293951..6757a3ff 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -52,16 +52,36 @@ const ( oidcValidatorCacheTTL = 15 * time.Minute // Constants related to conditions. - typeClientCredentialsValid = "ClientCredentialsValid" - typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" + typeClientCredentialsValid = "ClientCredentialsValid" + typeAdditionalAuthorizeParametersValid = "AdditionalAuthorizeParametersValid" + typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" - reasonUnreachable = "Unreachable" - reasonInvalidResponse = "InvalidResponse" + reasonUnreachable = "Unreachable" + reasonInvalidResponse = "InvalidResponse" + reasonDisallowedParameterName = "DisallowedParameterName" // Errors that are generated by our reconcile process. errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") ) +var ( + // Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters + // that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise + // happily treat the user's config as an override. Users can already set the "client_id" and "scope" params + // using other settings, and the others never make sense to override. This map should be treated as read-only + // since it is a global variable. + disallowedAdditionalAuthorizeParameters = map[string]bool{ //nolint: gochecknoglobals + "response_type": true, + "scope": true, + "client_id": true, + "state": true, + "nonce": true, + "code_challenge": true, + "code_challenge_method": true, + "redirect_uri": true, + } +) + // UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. type UpstreamOIDCIdentityProviderICache interface { SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI) @@ -167,21 +187,45 @@ func (c *oidcWatcherController) Sync(ctx controllerlib.Context) error { // validateUpstream validates the provided v1alpha1.OIDCIdentityProvider and returns the validated configuration as a // provider.UpstreamOIDCIdentityProvider. As a side effect, it also updates the status of the v1alpha1.OIDCIdentityProvider. func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig { + authorizationConfig := upstream.Spec.AuthorizationConfig + + additionalAuthcodeAuthorizeParameters := map[string]string{} + var rejectedAuthcodeAuthorizeParameters []string + for _, p := range authorizationConfig.AdditionalAuthorizeParameters { + if disallowedAdditionalAuthorizeParameters[p.Name] { + rejectedAuthcodeAuthorizeParameters = append(rejectedAuthcodeAuthorizeParameters, p.Name) + } else { + additionalAuthcodeAuthorizeParameters[p.Name] = p.Value + } + } + result := upstreamoidc.ProviderConfig{ Name: upstream.Name, Config: &oauth2.Config{ - Scopes: computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), + Scopes: computeScopes(authorizationConfig.AdditionalScopes, !authorizationConfig.DoNotRequestOfflineAccess), }, UsernameClaim: upstream.Spec.Claims.Username, GroupsClaim: upstream.Spec.Claims.Groups, - AllowPasswordGrant: upstream.Spec.AuthorizationConfig.AllowPasswordGrant, - AdditionalAuthcodeParams: map[string]string{"prompt": "consent"}, + AllowPasswordGrant: authorizationConfig.AllowPasswordGrant, + AdditionalAuthcodeParams: additionalAuthcodeAuthorizeParameters, ResourceUID: upstream.UID, } + conditions := []*v1alpha1.Condition{ c.validateSecret(upstream, &result), c.validateIssuer(ctx.Context, upstream, &result), } + if len(rejectedAuthcodeAuthorizeParameters) > 0 { + // This condition probably isn't important enough to report when it is successful, so just report errors. + conditions = append(conditions, &v1alpha1.Condition{ + Type: typeAdditionalAuthorizeParametersValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonDisallowedParameterName, + Message: fmt.Sprintf("the following additionalAuthorizeParameters are not allowed: %s", + strings.Join(rejectedAuthcodeAuthorizeParameters, ",")), + }) + } + c.updateStatus(ctx.Context, upstream, conditions) valid := true @@ -372,11 +416,13 @@ func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) return &result, nil } -func computeScopes(additionalScopes []string) []string { +func computeScopes(additionalScopes []string, includeOfflineAccess bool) []string { // First compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) set["openid"] = true - set["offline_access"] = true + if includeOfflineAccess { + set["offline_access"] = true + } for _, s := range additionalScopes { set[s] = true } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 73b0d2eb..8ac13c2c 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -124,8 +124,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { testName = "test-name" testSecretName = "test-client-secret" testAdditionalScopes = []string{"scope1", "scope2", "scope3"} - testExpectedScopes = []string{"offline_access", "openid", "scope1", "scope2", "scope3"} - testExpectedAdditionalParams = map[string]string{"prompt": "consent"} + testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"} + testAdditionalParams = []v1alpha1.Parameter{{Name: "prompt", Value: "consent"}, {Name: "foo", Value: "bar"}} + testExpectedAdditionalParams = map[string]string{"prompt": "consent", "foo": "bar"} testClientID = "test-oidc-client-id" testClientSecret = "test-oidc-client-secret" testValidSecretData = map[string][]byte{"clientID": []byte(testClientID), "clientSecret": []byte(testClientSecret)} @@ -150,10 +151,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL, - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{}, @@ -192,10 +192,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL, - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -238,10 +237,9 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL, - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -287,8 +285,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { TLS: &v1alpha1.TLSSpec{ CertificateAuthorityData: "invalid-base64", }, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -335,8 +332,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { TLS: &v1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")), }, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -379,9 +375,8 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -426,10 +421,9 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: wrongCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL + "/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: wrongCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -474,10 +468,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/invalid", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL + "/invalid", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -520,10 +513,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/insecure", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL + "/insecure", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -570,7 +562,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: append(testAdditionalScopes, "xyz", "openid", "offline_access"), + AdditionalScopes: append(testAdditionalScopes, "xyz", "openid", "offline_access"), // adds openid and offline_access unnecessarily AllowPasswordGrant: true, }, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, @@ -597,11 +589,11 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Name: testName, ClientID: testClientID, AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: append(testExpectedScopes, "xyz"), + Scopes: append(testExpectedScopes, "offline_access", "xyz"), // includes offline_access only once UsernameClaim: testUsernameClaim, GroupsClaim: testGroupsClaim, AllowPasswordGrant: true, - AdditionalAuthcodeParams: testExpectedAdditionalParams, + AdditionalAuthcodeParams: map[string]string{}, ResourceUID: testUID, }, }, @@ -624,9 +616,65 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Issuer: testIssuerURL, TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: testAdditionalScopes, - AllowPasswordGrant: false, + AdditionalScopes: testAdditionalScopes, // does not include offline_access + }, + }, + Status: v1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + }, + }, + }}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }}, + wantLogs: []string{ + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + }, + wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ + &oidctestutil.TestUpstreamOIDCIdentityProvider{ + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + Scopes: append(testExpectedScopes, "offline_access"), // gets offline_access by default + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: map[string]string{}, + ResourceUID: testUID, + }, + }, + wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: v1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + }, + }, + }}, + }, + { + name: "existing valid upstream with trailing slash and more optional settings", + inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: v1alpha1.OIDCIdentityProviderSpec{ + Issuer: testIssuerURL + "/ends-with-slash/", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ + DoNotRequestOfflineAccess: true, + AdditionalScopes: testAdditionalScopes, + AdditionalAuthorizeParameters: testAdditionalParams, + AllowPasswordGrant: true, }, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, }, @@ -652,10 +700,10 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Name: testName, ClientID: testClientID, AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: testExpectedScopes, + Scopes: testExpectedScopes, // does not include offline_access UsernameClaim: testUsernameClaim, GroupsClaim: testGroupsClaim, - AllowPasswordGrant: false, + AllowPasswordGrant: true, AdditionalAuthcodeParams: testExpectedAdditionalParams, ResourceUID: testUID, }, @@ -672,21 +720,25 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, }, { - name: "existing valid upstream with trailing slash", + name: "has disallowed additionalAuthorizeParams keys", inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/ends-with-slash/", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, - Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, - }, - Status: v1alpha1.OIDCIdentityProviderStatus{ - Phase: "Ready", - Conditions: []v1alpha1.Condition{ - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ + AdditionalAuthorizeParameters: []v1alpha1.Parameter{ + {Name: "response_type", Value: "foo"}, + {Name: "scope", Value: "foo"}, + {Name: "client_id", Value: "foo"}, + {Name: "state", Value: "foo"}, + {Name: "nonce", Value: "foo"}, + {Name: "code_challenge", Value: "foo"}, + {Name: "code_challenge_method", Value: "foo"}, + {Name: "redirect_uri", Value: "foo"}, + {Name: "this_one_is_allowed", Value: "foo"}, + }, }, }, }}, @@ -695,30 +747,24 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Type: "secrets.pinniped.dev/oidc-client", Data: testValidSecretData, }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri" "reason"="DisallowedParameterName" "status"="False" "type"="AdditionalAuthorizeParametersValid"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri" "name"="test-name" "namespace"="test-namespace" "reason"="DisallowedParameterName" "type"="AdditionalAuthorizeParametersValid"`, }, - wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ - &oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: testName, - ClientID: testClientID, - AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: testExpectedScopes, - UsernameClaim: testUsernameClaim, - GroupsClaim: testGroupsClaim, - AllowPasswordGrant: false, - AdditionalAuthcodeParams: testExpectedAdditionalParams, - ResourceUID: testUID, - }, - }, + wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, Status: v1alpha1.OIDCIdentityProviderStatus{ - Phase: "Ready", + Phase: "Error", Conditions: []v1alpha1.Condition{ - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, - {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + {Type: "AdditionalAuthorizeParametersValid", Status: "False", LastTransitionTime: now, Reason: "DisallowedParameterName", + Message: "the following additionalAuthorizeParameters are not allowed: " + + "response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri", ObservedGeneration: 1234}, + {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, }}, @@ -728,10 +774,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/ends-with-slash", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL + "/ends-with-slash", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -776,10 +821,9 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/", - TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, - Client: v1alpha1.OIDCClient{SecretName: testSecretName}, - AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes}, + Issuer: testIssuerURL + "/", + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, }}, inputSecrets: []runtime.Object{&corev1.Secret{ diff --git a/site/content/docs/howto/configure-supervisor-with-dex.md b/site/content/docs/howto/configure-supervisor-with-dex.md index db44d20e..723be7cf 100644 --- a/site/content/docs/howto/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/configure-supervisor-with-dex.md @@ -85,12 +85,20 @@ spec: # Specify the upstream issuer URL (no trailing slash). issuer: https:// - # Request any scopes other than "openid" for claims besides - # the default claims in your token. The "openid" scope is always - # included. + # Specify how to form authorization requests to Dex. authorizationConfig: + + # Request any scopes other than "openid" for claims besides + # the default claims in your token. The "openid" scope is always + # included. additionalScopes: [groups, email] + # If you would also like to allow your end users to authenticate using + # a password grant, then change this to true. + # Password grants with Dex will only work in Dex versions that include + # this bug fix: https://github.com/dexidp/dex/pull/2234 + allowPasswordGrant: false + # Specify how Dex claims are mapped to Kubernetes identities. claims: # Specify the name of the claim in your Dex ID token that will be mapped diff --git a/site/content/docs/howto/configure-supervisor-with-gitlab.md b/site/content/docs/howto/configure-supervisor-with-gitlab.md index fffb0a52..eb372dec 100644 --- a/site/content/docs/howto/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/configure-supervisor-with-gitlab.md @@ -30,8 +30,14 @@ For example, to create a user-owned application: 1. Create a new application: 1. Enter a name for your application, such as "My Kubernetes Clusters". 1. Enter the redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. - 1. Check the box saying that the application is _Confidential_. - 1. Select scope `openid`. This provides access to the `nickname` (GitLab username) and `groups` (GitLab groups) claims. + 1. Check the box saying that the application is _Confidential_. This is required and will cause GitLab to autogenerate + a client ID and client secret for your application. + 1. Check the box saying to _Expire Access Tokens_ to cause refresh tokens to be returned to the Supervisor. This is + required starting in Pinniped v0.13.0. + 1. Select scope `openid`. This is required to get ID tokens. Also, this provides access to the `nickname` (GitLab username) + and `groups` (GitLab groups) claims in the ID tokens. + 1. Optionally select other scopes which might provide access to other claims that you might want to use to determine + the usernames of your users, for example `email`. 1. Save the application and make note of the _Application ID_ and _Secret_. ## Configure the Supervisor cluster @@ -51,6 +57,19 @@ spec: # Specify the upstream issuer URL. issuer: https://gitlab.com + # Specify how to form authorization requests to GitLab. + authorizationConfig: + + # GitLab is unusual among OIDC providers in that it returns an + # error if you request the "offline_access" scope during an + # authorization flow, so ask Pinniped to avoid requesting that + # scope when using GitLab. + doNotRequestOfflineAccess: true + + # If you would also like to allow your end users to authenticate using + # a password grant, then change this to true. + allowPasswordGrant: false + # Specify how GitLab claims are mapped to Kubernetes identities. claims: diff --git a/site/content/docs/howto/configure-supervisor-with-okta.md b/site/content/docs/howto/configure-supervisor-with-okta.md index 6c01fb72..0890fa2f 100644 --- a/site/content/docs/howto/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/configure-supervisor-with-okta.md @@ -32,11 +32,18 @@ For example, to create an app: 1. Create a new app: 1. Click `Create App Integration`. 1. For `Sign-on method`, select `OIDC`. - 1. For `Application type`, app `Web Application`, then click next. + 1. For `Application type`, app `Web Application`, then click next. Only if you would like to offer the + password grant flow to your end users, then choose `Native Application` instead. 1. Enter a name for your app, such as "My Kubernetes Clusters". + 1. If you chose to create a `Web Application` then in the General Settings section, choose Grant Types + `Authorization Code` and `Refresh Token`. Starting in Pinniped v0.13.0, the `Refresh Token` grant is required. + 1. If you chose `Native Application` then in the General Settings section, choose Grant Types `Authorization Code`, + `Refresh Token`, and `Resource Owner Password`. Starting in Pinniped v0.13.0, the `Refresh Token` grant is required. 1. Enter the sign-in redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. 1. Optionally select `Limit access to selected groups` to restrict which Okta users can log in to Kubernetes using this integration. - 1. Save the app and make note of the _Client ID_ and _Client secret_. + 1. Save the app and make note of the _Client ID_ and _Client secret_. If you chose to create a `Native Application` + then there is an extra step required to get a client secret: after saving the app, in the + Client Credentials section click `Edit`, choose `Use Client Authentication`, and click `Save`. 1. Navigate to the _Sign On_ tab > _OpenID Connect ID Token_ and click `Edit`. Fill in the Groups claim filter. For example, for all groups to be present under the claim name `groups`, fill in "groups" in the first box, then select "Matches regex" and ".*". @@ -54,18 +61,26 @@ metadata: name: okta spec: - # Specify the upstream issuer URL (no trailing slash). + # Specify the upstream issuer URL (no trailing slash). Change this to be the + # actual issuer provided by your Okta account. issuer: https://my-company.okta.com - # Request any scopes other than "openid" for claims besides - # the default claims in your token. The "openid" scope is always - # included. - # - # To learn more about how to customize the claims returned, see here: - # https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/overview/ + # Specify how to form authorization requests to Okta. authorizationConfig: + + # Request any scopes other than "openid" for claims besides + # the default claims in your token. The "openid" scope is always + # included. + # + # To learn more about how to customize the claims returned, see here: + # https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/overview/ additionalScopes: [groups, email] + # If you would also like to allow your end users to authenticate using + # a password grant, then change this to true. Password grants only work + # with applications created in Okta as "Native Applications". + allowPasswordGrant: false + # Specify how Okta claims are mapped to Kubernetes identities. claims: From c51d7c08b9e2c9d25a81c3fb635af53b5acd1592 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 18 Oct 2021 15:35:22 -0700 Subject: [PATCH 08/19] Add a comment that might be useful some day --- internal/crud/crud.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/crud/crud.go b/internal/crud/crud.go index 84abe142..4211ae10 100644 --- a/internal/crud/crud.go +++ b/internal/crud/crud.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package crud @@ -109,6 +109,10 @@ func (s *secretsStorage) validateSecret(secret *corev1.Secret) error { } func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) { + // Note: There may be a small bug here in that toSecret will move the SecretLifetimeAnnotationKey date forward + // instead of keeping the storage resource's original SecretLifetimeAnnotationKey value. However, we only use + // this Update method in one place, and it doesn't matter in that place. Be aware that it might need improvement + // if we start using this Update method in more places. secret, err := s.toSecret(signature, resourceVersion, data, nil) if err != nil { return "", err From c43e019d3a64272e752497e0a0f5f3dc89cc678e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 18 Oct 2021 16:41:31 -0700 Subject: [PATCH 09/19] Change default of additionalScopes and disallow "hd" in additionalAuthorizeParameters --- .../types_oidcidentityprovider.go.tmpl | 79 ++++---- ...or.pinniped.dev_oidcidentityproviders.yaml | 180 +++++++++--------- generated/1.17/README.adoc | 7 +- .../v1alpha1/types_oidcidentityprovider.go | 79 ++++---- ...or.pinniped.dev_oidcidentityproviders.yaml | 180 +++++++++--------- generated/1.18/README.adoc | 7 +- .../v1alpha1/types_oidcidentityprovider.go | 79 ++++---- ...or.pinniped.dev_oidcidentityproviders.yaml | 180 +++++++++--------- generated/1.19/README.adoc | 7 +- .../v1alpha1/types_oidcidentityprovider.go | 79 ++++---- ...or.pinniped.dev_oidcidentityproviders.yaml | 180 +++++++++--------- generated/1.20/README.adoc | 7 +- .../v1alpha1/types_oidcidentityprovider.go | 79 ++++---- ...or.pinniped.dev_oidcidentityproviders.yaml | 180 +++++++++--------- .../v1alpha1/types_oidcidentityprovider.go | 79 ++++---- .../oidc_upstream_watcher.go | 29 +-- .../oidc_upstream_watcher_test.go | 76 ++++++-- 17 files changed, 773 insertions(+), 734 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl index 9e0624a0..bae993d0 100644 --- a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 390f4848..14f6fd89 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -56,28 +56,99 @@ spec: the OAuth2 authorization request parameters to be used with this OIDC identity provider. properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map additionalScopes: - description: AdditionalScopes are the additional scopes that will - be requested from your OIDC provider in the authorization request - during an OIDC Authorization Code Flow and in the token request - during a Resource Owner Password Credentials Grant. Note that - the "openid" scope will always be requested regardless of the - value in this setting, since it is always required according - to the OIDC spec. The "offline_access" scope may also be included - according to the value of the DoNotRequestOfflineAccess setting. - Any other scopes required should be included here in the AdditionalScopes - list. For example, you might like to include scopes like "profile", - "email", or "groups" in order to receive the related claims - in the returned ID token or userinfo endpoint results if you - would like to make use of those claims in the OIDCClaims settings - to determine the usernames and group memberships of your Kubernetes - users. See your OIDC provider's documentation for more information - about what scopes are available to request claims. + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. By setting + this list to anything other than an empty list, you are overriding + the default value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. Some OIDC providers + may also require a scope to get access to the user''s group + membership, in which case you may wish to include it in this + list. Sometimes the scope to request the user''s group membership + is called "groups", but unfortunately this is not specified + in the OIDC standard. Generally speaking, you should include + any scopes required to cause the appropriate claims to be the + returned by your OIDC provider in the ID token or userinfo endpoint + results for those claims which you would like to use in the + oidcClaims settings to determine the usernames and group memberships + of your Kubernetes users. See your OIDC provider''s documentation + for more information about what scopes are available to request + claims. Additionally, the Pinniped Supervisor requires that + your OIDC provider returns refresh tokens to the Supervisor + from these authorization flows. For most OIDC providers, the + scope required to receive refresh tokens will be "offline_access". + See the documentation of your OIDC provider''s authorization + and token endpoints for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. Note that it may be safe to send "offline_access" + even to providers which do not require it, since the provider + may ignore scopes that it does not understand or require (see + https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' items: type: string type: array allowPasswordGrant: - description: AllowPasswordGrant, when true, will allow the use + description: allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password @@ -103,82 +174,9 @@ spec: your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider - during Resource Owner Password Credentials Grant logins. AllowPasswordGrant + during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. type: boolean - doNotRequestOfflineAccess: - description: DoNotRequestOfflineAccess determines if the "offline_access" - scope will be requested from your OIDC provider in the authorization - request during an OIDC Authorization Code Flow and in the token - request during a Resource Owner Password Credentials Grant in - order to ask to receive a refresh token in the response. Starting - in v0.13.0, the Pinniped Supervisor requires that your OIDC - provider returns refresh tokens to the Supervisor from these - authorization flows. For most OIDC providers, the scope required - to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. See the documentation - of your OIDC provider's authorization and token endpoints for - its requirements for what to include in the request in order - to receive a refresh token in the response, if anything. By - default, DoNotRequestOfflineAccess is false, which means that - "offline_access" will be sent in the authorization request, - since that is what is suggested by the OIDC specification. Note - that it may be safe to send "offline_access" even to providers - which do not require it, since the provider may ignore scopes - that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). - In the unusual case that you must avoid sending the "offline_access" - scope, set DoNotRequestOfflineAccess to true. This is required - if your OIDC provider will reject the request when it includes - "offline_access" (e.g. GitLab's OIDC provider). If you need - to send some other scope to request a refresh token, include - the scope name in the additionalScopes setting. Also note that - some OIDC providers may require that the "prompt" param be set - to a specific value for the authorization request during an - OIDC Authorization Code Flow in order to receive a refresh token - in the response. To adjust the prompt param, see the additionalAuthorizeParameters - setting. - type: boolean - extraAuthorizeParameters: - description: AdditionalAuthorizeParameters are extra query parameters - that should be included in the authorize request to your OIDC - provider in the authorization request during an OIDC Authorization - Code Flow. By default, no extra parameters are sent. The standard - parameters that will be sent are "response_type", "scope", "client_id", - "state", "nonce", "code_challenge", "code_challenge_method", - and "redirect_uri". These parameters cannot be included in this - setting. This setting does not influence the parameters sent - to the token endpoint in the Resource Owner Password Credentials - Grant. Starting in v0.13.0, the Pinniped Supervisor requires - that your OIDC provider returns refresh tokens to the Supervisor - from the authorization flows. Some OIDC providers may require - a certain value for the "prompt" parameter in order to properly - request refresh tokens. See the documentation of your OIDC provider's - authorization endpoint for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. If your provider requires the prompt parameter - to request a refresh token, then include it here. Also note - that most providers also require a certain scope to be requested - in order to receive refresh tokens. See the doNotRequestOfflineAccess - setting for more information about using scopes to request refresh - tokens. - items: - description: Parameter is a key/value pair which represents - a parameter in an HTTP request. - properties: - name: - description: The name of the parameter. Required. - minLength: 1 - type: string - value: - description: The value of the parameter. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index e3c28fbf..35b16323 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -1099,10 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. -| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. -| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. -| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. +| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 9e0624a0..bae993d0 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 390f4848..14f6fd89 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -56,28 +56,99 @@ spec: the OAuth2 authorization request parameters to be used with this OIDC identity provider. properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map additionalScopes: - description: AdditionalScopes are the additional scopes that will - be requested from your OIDC provider in the authorization request - during an OIDC Authorization Code Flow and in the token request - during a Resource Owner Password Credentials Grant. Note that - the "openid" scope will always be requested regardless of the - value in this setting, since it is always required according - to the OIDC spec. The "offline_access" scope may also be included - according to the value of the DoNotRequestOfflineAccess setting. - Any other scopes required should be included here in the AdditionalScopes - list. For example, you might like to include scopes like "profile", - "email", or "groups" in order to receive the related claims - in the returned ID token or userinfo endpoint results if you - would like to make use of those claims in the OIDCClaims settings - to determine the usernames and group memberships of your Kubernetes - users. See your OIDC provider's documentation for more information - about what scopes are available to request claims. + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. By setting + this list to anything other than an empty list, you are overriding + the default value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. Some OIDC providers + may also require a scope to get access to the user''s group + membership, in which case you may wish to include it in this + list. Sometimes the scope to request the user''s group membership + is called "groups", but unfortunately this is not specified + in the OIDC standard. Generally speaking, you should include + any scopes required to cause the appropriate claims to be the + returned by your OIDC provider in the ID token or userinfo endpoint + results for those claims which you would like to use in the + oidcClaims settings to determine the usernames and group memberships + of your Kubernetes users. See your OIDC provider''s documentation + for more information about what scopes are available to request + claims. Additionally, the Pinniped Supervisor requires that + your OIDC provider returns refresh tokens to the Supervisor + from these authorization flows. For most OIDC providers, the + scope required to receive refresh tokens will be "offline_access". + See the documentation of your OIDC provider''s authorization + and token endpoints for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. Note that it may be safe to send "offline_access" + even to providers which do not require it, since the provider + may ignore scopes that it does not understand or require (see + https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' items: type: string type: array allowPasswordGrant: - description: AllowPasswordGrant, when true, will allow the use + description: allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password @@ -103,82 +174,9 @@ spec: your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider - during Resource Owner Password Credentials Grant logins. AllowPasswordGrant + during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. type: boolean - doNotRequestOfflineAccess: - description: DoNotRequestOfflineAccess determines if the "offline_access" - scope will be requested from your OIDC provider in the authorization - request during an OIDC Authorization Code Flow and in the token - request during a Resource Owner Password Credentials Grant in - order to ask to receive a refresh token in the response. Starting - in v0.13.0, the Pinniped Supervisor requires that your OIDC - provider returns refresh tokens to the Supervisor from these - authorization flows. For most OIDC providers, the scope required - to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. See the documentation - of your OIDC provider's authorization and token endpoints for - its requirements for what to include in the request in order - to receive a refresh token in the response, if anything. By - default, DoNotRequestOfflineAccess is false, which means that - "offline_access" will be sent in the authorization request, - since that is what is suggested by the OIDC specification. Note - that it may be safe to send "offline_access" even to providers - which do not require it, since the provider may ignore scopes - that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). - In the unusual case that you must avoid sending the "offline_access" - scope, set DoNotRequestOfflineAccess to true. This is required - if your OIDC provider will reject the request when it includes - "offline_access" (e.g. GitLab's OIDC provider). If you need - to send some other scope to request a refresh token, include - the scope name in the additionalScopes setting. Also note that - some OIDC providers may require that the "prompt" param be set - to a specific value for the authorization request during an - OIDC Authorization Code Flow in order to receive a refresh token - in the response. To adjust the prompt param, see the additionalAuthorizeParameters - setting. - type: boolean - extraAuthorizeParameters: - description: AdditionalAuthorizeParameters are extra query parameters - that should be included in the authorize request to your OIDC - provider in the authorization request during an OIDC Authorization - Code Flow. By default, no extra parameters are sent. The standard - parameters that will be sent are "response_type", "scope", "client_id", - "state", "nonce", "code_challenge", "code_challenge_method", - and "redirect_uri". These parameters cannot be included in this - setting. This setting does not influence the parameters sent - to the token endpoint in the Resource Owner Password Credentials - Grant. Starting in v0.13.0, the Pinniped Supervisor requires - that your OIDC provider returns refresh tokens to the Supervisor - from the authorization flows. Some OIDC providers may require - a certain value for the "prompt" parameter in order to properly - request refresh tokens. See the documentation of your OIDC provider's - authorization endpoint for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. If your provider requires the prompt parameter - to request a refresh token, then include it here. Also note - that most providers also require a certain scope to be requested - in order to receive refresh tokens. See the doNotRequestOfflineAccess - setting for more information about using scopes to request refresh - tokens. - items: - description: Parameter is a key/value pair which represents - a parameter in an HTTP request. - properties: - name: - description: The name of the parameter. Required. - minLength: 1 - type: string - value: - description: The value of the parameter. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index c08b2d5e..2f383af3 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -1099,10 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. -| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. -| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. -| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. +| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 9e0624a0..bae993d0 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 390f4848..14f6fd89 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -56,28 +56,99 @@ spec: the OAuth2 authorization request parameters to be used with this OIDC identity provider. properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map additionalScopes: - description: AdditionalScopes are the additional scopes that will - be requested from your OIDC provider in the authorization request - during an OIDC Authorization Code Flow and in the token request - during a Resource Owner Password Credentials Grant. Note that - the "openid" scope will always be requested regardless of the - value in this setting, since it is always required according - to the OIDC spec. The "offline_access" scope may also be included - according to the value of the DoNotRequestOfflineAccess setting. - Any other scopes required should be included here in the AdditionalScopes - list. For example, you might like to include scopes like "profile", - "email", or "groups" in order to receive the related claims - in the returned ID token or userinfo endpoint results if you - would like to make use of those claims in the OIDCClaims settings - to determine the usernames and group memberships of your Kubernetes - users. See your OIDC provider's documentation for more information - about what scopes are available to request claims. + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. By setting + this list to anything other than an empty list, you are overriding + the default value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. Some OIDC providers + may also require a scope to get access to the user''s group + membership, in which case you may wish to include it in this + list. Sometimes the scope to request the user''s group membership + is called "groups", but unfortunately this is not specified + in the OIDC standard. Generally speaking, you should include + any scopes required to cause the appropriate claims to be the + returned by your OIDC provider in the ID token or userinfo endpoint + results for those claims which you would like to use in the + oidcClaims settings to determine the usernames and group memberships + of your Kubernetes users. See your OIDC provider''s documentation + for more information about what scopes are available to request + claims. Additionally, the Pinniped Supervisor requires that + your OIDC provider returns refresh tokens to the Supervisor + from these authorization flows. For most OIDC providers, the + scope required to receive refresh tokens will be "offline_access". + See the documentation of your OIDC provider''s authorization + and token endpoints for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. Note that it may be safe to send "offline_access" + even to providers which do not require it, since the provider + may ignore scopes that it does not understand or require (see + https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' items: type: string type: array allowPasswordGrant: - description: AllowPasswordGrant, when true, will allow the use + description: allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password @@ -103,82 +174,9 @@ spec: your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider - during Resource Owner Password Credentials Grant logins. AllowPasswordGrant + during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. type: boolean - doNotRequestOfflineAccess: - description: DoNotRequestOfflineAccess determines if the "offline_access" - scope will be requested from your OIDC provider in the authorization - request during an OIDC Authorization Code Flow and in the token - request during a Resource Owner Password Credentials Grant in - order to ask to receive a refresh token in the response. Starting - in v0.13.0, the Pinniped Supervisor requires that your OIDC - provider returns refresh tokens to the Supervisor from these - authorization flows. For most OIDC providers, the scope required - to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. See the documentation - of your OIDC provider's authorization and token endpoints for - its requirements for what to include in the request in order - to receive a refresh token in the response, if anything. By - default, DoNotRequestOfflineAccess is false, which means that - "offline_access" will be sent in the authorization request, - since that is what is suggested by the OIDC specification. Note - that it may be safe to send "offline_access" even to providers - which do not require it, since the provider may ignore scopes - that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). - In the unusual case that you must avoid sending the "offline_access" - scope, set DoNotRequestOfflineAccess to true. This is required - if your OIDC provider will reject the request when it includes - "offline_access" (e.g. GitLab's OIDC provider). If you need - to send some other scope to request a refresh token, include - the scope name in the additionalScopes setting. Also note that - some OIDC providers may require that the "prompt" param be set - to a specific value for the authorization request during an - OIDC Authorization Code Flow in order to receive a refresh token - in the response. To adjust the prompt param, see the additionalAuthorizeParameters - setting. - type: boolean - extraAuthorizeParameters: - description: AdditionalAuthorizeParameters are extra query parameters - that should be included in the authorize request to your OIDC - provider in the authorization request during an OIDC Authorization - Code Flow. By default, no extra parameters are sent. The standard - parameters that will be sent are "response_type", "scope", "client_id", - "state", "nonce", "code_challenge", "code_challenge_method", - and "redirect_uri". These parameters cannot be included in this - setting. This setting does not influence the parameters sent - to the token endpoint in the Resource Owner Password Credentials - Grant. Starting in v0.13.0, the Pinniped Supervisor requires - that your OIDC provider returns refresh tokens to the Supervisor - from the authorization flows. Some OIDC providers may require - a certain value for the "prompt" parameter in order to properly - request refresh tokens. See the documentation of your OIDC provider's - authorization endpoint for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. If your provider requires the prompt parameter - to request a refresh token, then include it here. Also note - that most providers also require a certain scope to be requested - in order to receive refresh tokens. See the doNotRequestOfflineAccess - setting for more information about using scopes to request refresh - tokens. - items: - description: Parameter is a key/value pair which represents - a parameter in an HTTP request. - properties: - name: - description: The name of the parameter. Required. - minLength: 1 - type: string - value: - description: The value of the parameter. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index f3d0aa51..0dace9e2 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -1099,10 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. -| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. -| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. -| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. +| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 9e0624a0..bae993d0 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 390f4848..14f6fd89 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -56,28 +56,99 @@ spec: the OAuth2 authorization request parameters to be used with this OIDC identity provider. properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map additionalScopes: - description: AdditionalScopes are the additional scopes that will - be requested from your OIDC provider in the authorization request - during an OIDC Authorization Code Flow and in the token request - during a Resource Owner Password Credentials Grant. Note that - the "openid" scope will always be requested regardless of the - value in this setting, since it is always required according - to the OIDC spec. The "offline_access" scope may also be included - according to the value of the DoNotRequestOfflineAccess setting. - Any other scopes required should be included here in the AdditionalScopes - list. For example, you might like to include scopes like "profile", - "email", or "groups" in order to receive the related claims - in the returned ID token or userinfo endpoint results if you - would like to make use of those claims in the OIDCClaims settings - to determine the usernames and group memberships of your Kubernetes - users. See your OIDC provider's documentation for more information - about what scopes are available to request claims. + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. By setting + this list to anything other than an empty list, you are overriding + the default value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. Some OIDC providers + may also require a scope to get access to the user''s group + membership, in which case you may wish to include it in this + list. Sometimes the scope to request the user''s group membership + is called "groups", but unfortunately this is not specified + in the OIDC standard. Generally speaking, you should include + any scopes required to cause the appropriate claims to be the + returned by your OIDC provider in the ID token or userinfo endpoint + results for those claims which you would like to use in the + oidcClaims settings to determine the usernames and group memberships + of your Kubernetes users. See your OIDC provider''s documentation + for more information about what scopes are available to request + claims. Additionally, the Pinniped Supervisor requires that + your OIDC provider returns refresh tokens to the Supervisor + from these authorization flows. For most OIDC providers, the + scope required to receive refresh tokens will be "offline_access". + See the documentation of your OIDC provider''s authorization + and token endpoints for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. Note that it may be safe to send "offline_access" + even to providers which do not require it, since the provider + may ignore scopes that it does not understand or require (see + https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' items: type: string type: array allowPasswordGrant: - description: AllowPasswordGrant, when true, will allow the use + description: allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password @@ -103,82 +174,9 @@ spec: your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider - during Resource Owner Password Credentials Grant logins. AllowPasswordGrant + during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. type: boolean - doNotRequestOfflineAccess: - description: DoNotRequestOfflineAccess determines if the "offline_access" - scope will be requested from your OIDC provider in the authorization - request during an OIDC Authorization Code Flow and in the token - request during a Resource Owner Password Credentials Grant in - order to ask to receive a refresh token in the response. Starting - in v0.13.0, the Pinniped Supervisor requires that your OIDC - provider returns refresh tokens to the Supervisor from these - authorization flows. For most OIDC providers, the scope required - to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. See the documentation - of your OIDC provider's authorization and token endpoints for - its requirements for what to include in the request in order - to receive a refresh token in the response, if anything. By - default, DoNotRequestOfflineAccess is false, which means that - "offline_access" will be sent in the authorization request, - since that is what is suggested by the OIDC specification. Note - that it may be safe to send "offline_access" even to providers - which do not require it, since the provider may ignore scopes - that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). - In the unusual case that you must avoid sending the "offline_access" - scope, set DoNotRequestOfflineAccess to true. This is required - if your OIDC provider will reject the request when it includes - "offline_access" (e.g. GitLab's OIDC provider). If you need - to send some other scope to request a refresh token, include - the scope name in the additionalScopes setting. Also note that - some OIDC providers may require that the "prompt" param be set - to a specific value for the authorization request during an - OIDC Authorization Code Flow in order to receive a refresh token - in the response. To adjust the prompt param, see the additionalAuthorizeParameters - setting. - type: boolean - extraAuthorizeParameters: - description: AdditionalAuthorizeParameters are extra query parameters - that should be included in the authorize request to your OIDC - provider in the authorization request during an OIDC Authorization - Code Flow. By default, no extra parameters are sent. The standard - parameters that will be sent are "response_type", "scope", "client_id", - "state", "nonce", "code_challenge", "code_challenge_method", - and "redirect_uri". These parameters cannot be included in this - setting. This setting does not influence the parameters sent - to the token endpoint in the Resource Owner Password Credentials - Grant. Starting in v0.13.0, the Pinniped Supervisor requires - that your OIDC provider returns refresh tokens to the Supervisor - from the authorization flows. Some OIDC providers may require - a certain value for the "prompt" parameter in order to properly - request refresh tokens. See the documentation of your OIDC provider's - authorization endpoint for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. If your provider requires the prompt parameter - to request a refresh token, then include it here. Also note - that most providers also require a certain scope to be requested - in order to receive refresh tokens. See the doNotRequestOfflineAccess - setting for more information about using scopes to request refresh - tokens. - items: - description: Parameter is a key/value pair which represents - a parameter in an HTTP request. - properties: - name: - description: The name of the parameter. Required. - minLength: 1 - type: string - value: - description: The value of the parameter. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index b78ae013..cbdcb6fd 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -1099,10 +1099,9 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`doNotRequestOfflineAccess`* __boolean__ | DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. By default, DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC providers may require that the "prompt" param be set to a specific value for the authorization request during an OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see the additionalAuthorizeParameters setting. -| *`additionalScopes`* __string array__ | AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. The "offline_access" scope may also be included according to the value of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the related claims in the returned ID token or userinfo endpoint results if you would like to make use of those claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. -| *`extraAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about using scopes to request refresh tokens. -| *`allowPasswordGrant`* __boolean__ | AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. AllowPasswordGrant defaults to false. +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. +| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 9e0624a0..bae993d0 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 390f4848..14f6fd89 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -56,28 +56,99 @@ spec: the OAuth2 authorization request parameters to be used with this OIDC identity provider. properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map additionalScopes: - description: AdditionalScopes are the additional scopes that will - be requested from your OIDC provider in the authorization request - during an OIDC Authorization Code Flow and in the token request - during a Resource Owner Password Credentials Grant. Note that - the "openid" scope will always be requested regardless of the - value in this setting, since it is always required according - to the OIDC spec. The "offline_access" scope may also be included - according to the value of the DoNotRequestOfflineAccess setting. - Any other scopes required should be included here in the AdditionalScopes - list. For example, you might like to include scopes like "profile", - "email", or "groups" in order to receive the related claims - in the returned ID token or userinfo endpoint results if you - would like to make use of those claims in the OIDCClaims settings - to determine the usernames and group memberships of your Kubernetes - users. See your OIDC provider's documentation for more information - about what scopes are available to request claims. + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. By setting + this list to anything other than an empty list, you are overriding + the default value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. Some OIDC providers + may also require a scope to get access to the user''s group + membership, in which case you may wish to include it in this + list. Sometimes the scope to request the user''s group membership + is called "groups", but unfortunately this is not specified + in the OIDC standard. Generally speaking, you should include + any scopes required to cause the appropriate claims to be the + returned by your OIDC provider in the ID token or userinfo endpoint + results for those claims which you would like to use in the + oidcClaims settings to determine the usernames and group memberships + of your Kubernetes users. See your OIDC provider''s documentation + for more information about what scopes are available to request + claims. Additionally, the Pinniped Supervisor requires that + your OIDC provider returns refresh tokens to the Supervisor + from these authorization flows. For most OIDC providers, the + scope required to receive refresh tokens will be "offline_access". + See the documentation of your OIDC provider''s authorization + and token endpoints for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. Note that it may be safe to send "offline_access" + even to providers which do not require it, since the provider + may ignore scopes that it does not understand or require (see + https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' items: type: string type: array allowPasswordGrant: - description: AllowPasswordGrant, when true, will allow the use + description: allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password @@ -103,82 +174,9 @@ spec: your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider - during Resource Owner Password Credentials Grant logins. AllowPasswordGrant + during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. type: boolean - doNotRequestOfflineAccess: - description: DoNotRequestOfflineAccess determines if the "offline_access" - scope will be requested from your OIDC provider in the authorization - request during an OIDC Authorization Code Flow and in the token - request during a Resource Owner Password Credentials Grant in - order to ask to receive a refresh token in the response. Starting - in v0.13.0, the Pinniped Supervisor requires that your OIDC - provider returns refresh tokens to the Supervisor from these - authorization flows. For most OIDC providers, the scope required - to receive refresh tokens will be "offline_access". See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. See the documentation - of your OIDC provider's authorization and token endpoints for - its requirements for what to include in the request in order - to receive a refresh token in the response, if anything. By - default, DoNotRequestOfflineAccess is false, which means that - "offline_access" will be sent in the authorization request, - since that is what is suggested by the OIDC specification. Note - that it may be safe to send "offline_access" even to providers - which do not require it, since the provider may ignore scopes - that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). - In the unusual case that you must avoid sending the "offline_access" - scope, set DoNotRequestOfflineAccess to true. This is required - if your OIDC provider will reject the request when it includes - "offline_access" (e.g. GitLab's OIDC provider). If you need - to send some other scope to request a refresh token, include - the scope name in the additionalScopes setting. Also note that - some OIDC providers may require that the "prompt" param be set - to a specific value for the authorization request during an - OIDC Authorization Code Flow in order to receive a refresh token - in the response. To adjust the prompt param, see the additionalAuthorizeParameters - setting. - type: boolean - extraAuthorizeParameters: - description: AdditionalAuthorizeParameters are extra query parameters - that should be included in the authorize request to your OIDC - provider in the authorization request during an OIDC Authorization - Code Flow. By default, no extra parameters are sent. The standard - parameters that will be sent are "response_type", "scope", "client_id", - "state", "nonce", "code_challenge", "code_challenge_method", - and "redirect_uri". These parameters cannot be included in this - setting. This setting does not influence the parameters sent - to the token endpoint in the Resource Owner Password Credentials - Grant. Starting in v0.13.0, the Pinniped Supervisor requires - that your OIDC provider returns refresh tokens to the Supervisor - from the authorization flows. Some OIDC providers may require - a certain value for the "prompt" parameter in order to properly - request refresh tokens. See the documentation of your OIDC provider's - authorization endpoint for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. If your provider requires the prompt parameter - to request a refresh token, then include it here. Also note - that most providers also require a certain scope to be requested - in order to receive refresh tokens. See the doNotRequestOfflineAccess - setting for more information about using scopes to request refresh - tokens. - items: - description: Parameter is a key/value pair which represents - a parameter in an HTTP request. - properties: - name: - description: The name of the parameter. Required. - minLength: 1 - type: string - value: - description: The value of the parameter. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map type: object claims: description: Claims provides the names of token claims that will be diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index 9e0624a0..bae993d0 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -38,60 +38,59 @@ type OIDCIdentityProviderStatus struct { // OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization // request parameters. type OIDCAuthorizationConfig struct { - // DoNotRequestOfflineAccess determines if the "offline_access" scope will be requested from your OIDC provider in - // the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner - // Password Credentials Grant in order to ask to receive a refresh token in the response. Starting in v0.13.0, the - // Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these - // authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". - // See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" - // scope. See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what - // to include in the request in order to receive a refresh token in the response, if anything. By default, - // DoNotRequestOfflineAccess is false, which means that "offline_access" will be sent in the authorization request, - // since that is what is suggested by the OIDC specification. Note that it may be safe to send "offline_access" even to - // providers which do not require it, since the provider may ignore scopes that it does not understand or require (see - // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the - // "offline_access" scope, set DoNotRequestOfflineAccess to true. This is required if your OIDC provider will reject - // the request when it includes "offline_access" (e.g. GitLab's OIDC provider). If you need to send some other scope - // to request a refresh token, include the scope name in the additionalScopes setting. Also note that some OIDC - // providers may require that the "prompt" param be set to a specific value for the authorization request during an - // OIDC Authorization Code Flow in order to receive a refresh token in the response. To adjust the prompt param, see - // the additionalAuthorizeParameters setting. - // +optional - DoNotRequestOfflineAccess bool `json:"doNotRequestOfflineAccess,omitempty"` - - // AdditionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is - // always required according to the OIDC spec. The "offline_access" scope may also be included according to the value - // of the DoNotRequestOfflineAccess setting. Any other scopes required should be included here in the AdditionalScopes - // list. For example, you might like to include scopes like "profile", "email", or "groups" in order to receive the - // related claims in the returned ID token or userinfo endpoint results if you would like to make use of those - // claims in the OIDCClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). // +optional AdditionalScopes []string `json:"additionalScopes,omitempty"` - // AdditionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be - // included in this setting. This setting does not influence the parameters sent to the token endpoint in the - // Resource Owner Password Credentials Grant. Starting in v0.13.0, the Pinniped Supervisor requires that your OIDC - // provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require - // a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of - // your OIDC provider's authorization endpoint for its requirements for what to include in the request in - // order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to - // request a refresh token, then include it here. Also note that most providers also require a certain scope to be - // requested in order to receive refresh tokens. See the doNotRequestOfflineAccess setting for more information about - // using scopes to request refresh tokens. + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. // +optional // +patchMergeKey=name // +patchStrategy=merge // +listType=map // +listMapKey=name - AdditionalAuthorizeParameters []Parameter `json:"extraAuthorizeParameters,omitempty"` + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` - // AllowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be @@ -106,7 +105,7 @@ type OIDCAuthorizationConfig struct { // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. - // AllowPasswordGrant defaults to false. + // allowPasswordGrant defaults to false. // +optional AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 6757a3ff..9719be44 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -65,12 +65,12 @@ const ( ) var ( - // Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters - // that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise - // happily treat the user's config as an override. Users can already set the "client_id" and "scope" params - // using other settings, and the others never make sense to override. This map should be treated as read-only - // since it is a global variable. disallowedAdditionalAuthorizeParameters = map[string]bool{ //nolint: gochecknoglobals + // Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters + // that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise + // happily treat the user's config as an override. Users can already set the "client_id" and "scope" params + // using other settings, and the others never make sense to override. This map should be treated as read-only + // since it is a global variable. "response_type": true, "scope": true, "client_id": true, @@ -79,6 +79,10 @@ var ( "code_challenge": true, "code_challenge_method": true, "redirect_uri": true, + + // Reject "hd" for now because it is not safe to use with Google's OIDC provider until Pinniped also + // performs the corresponding validation on the ID token. + "hd": true, } ) @@ -202,7 +206,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst result := upstreamoidc.ProviderConfig{ Name: upstream.Name, Config: &oauth2.Config{ - Scopes: computeScopes(authorizationConfig.AdditionalScopes, !authorizationConfig.DoNotRequestOfflineAccess), + Scopes: computeScopes(authorizationConfig.AdditionalScopes), }, UsernameClaim: upstream.Spec.Claims.Username, GroupsClaim: upstream.Spec.Claims.Groups, @@ -416,13 +420,15 @@ func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) return &result, nil } -func computeScopes(additionalScopes []string, includeOfflineAccess bool) []string { - // First compute the unique set of scopes, including "openid" (de-duplicate). +func computeScopes(additionalScopes []string) []string { + // If none are set then provide a reasonable default which only tries to use scopes defined in the OIDC spec. + if len(additionalScopes) == 0 { + return []string{"openid", "offline_access", "email", "profile"} + } + + // Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) set["openid"] = true - if includeOfflineAccess { - set["offline_access"] = true - } for _, s := range additionalScopes { set[s] = true } @@ -433,6 +439,7 @@ func computeScopes(additionalScopes []string, includeOfflineAccess bool) []strin scopes = append(scopes, s) } sort.Strings(scopes) + return scopes } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 8ac13c2c..dc61c63d 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -125,6 +125,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { testSecretName = "test-client-secret" testAdditionalScopes = []string{"scope1", "scope2", "scope3"} testExpectedScopes = []string{"openid", "scope1", "scope2", "scope3"} + testDefaultExpectedScopes = []string{"openid", "offline_access", "email", "profile"} testAdditionalParams = []v1alpha1.Parameter{{Name: "prompt", Value: "consent"}, {Name: "foo", Value: "bar"}} testExpectedAdditionalParams = map[string]string{"prompt": "consent", "foo": "bar"} testClientID = "test-oidc-client-id" @@ -562,7 +563,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: append(testAdditionalScopes, "xyz", "openid", "offline_access"), // adds openid and offline_access unnecessarily + AdditionalScopes: append(testAdditionalScopes, "xyz", "openid"), // adds openid unnecessarily AllowPasswordGrant: true, }, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, @@ -589,7 +590,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Name: testName, ClientID: testClientID, AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: append(testExpectedScopes, "offline_access", "xyz"), // includes offline_access only once + Scopes: append(testExpectedScopes, "xyz"), // includes openid only once UsernameClaim: testUsernameClaim, GroupsClaim: testGroupsClaim, AllowPasswordGrant: true, @@ -609,7 +610,58 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, }, { - name: "existing valid upstream", + name: "existing valid upstream with default authorizationConfig", + inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: v1alpha1.OIDCIdentityProviderSpec{ + Issuer: testIssuerURL, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, + Client: v1alpha1.OIDCClient{SecretName: testSecretName}, + Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, + }, + Status: v1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, + }, + }, + }}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName}, + Type: "secrets.pinniped.dev/oidc-client", + Data: testValidSecretData, + }}, + wantLogs: []string{ + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + }, + wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ + &oidctestutil.TestUpstreamOIDCIdentityProvider{ + Name: testName, + ClientID: testClientID, + AuthorizationURL: *testIssuerAuthorizeURL, + Scopes: testDefaultExpectedScopes, + UsernameClaim: testUsernameClaim, + GroupsClaim: testGroupsClaim, + AllowPasswordGrant: false, + AdditionalAuthcodeParams: map[string]string{}, + ResourceUID: testUID, + }, + }, + wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: v1alpha1.OIDCIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, + }, + }, + }}, + }, + { + name: "existing valid upstream with additionalScopes set to override the default", inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, Spec: v1alpha1.OIDCIdentityProviderSpec{ @@ -618,7 +670,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Client: v1alpha1.OIDCClient{SecretName: testSecretName}, Claims: v1alpha1.OIDCClaims{Groups: testGroupsClaim, Username: testUsernameClaim}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: testAdditionalScopes, // does not include offline_access + AdditionalScopes: testAdditionalScopes, }, }, Status: v1alpha1.OIDCIdentityProviderStatus{ @@ -643,7 +695,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Name: testName, ClientID: testClientID, AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: append(testExpectedScopes, "offline_access"), // gets offline_access by default + Scopes: testExpectedScopes, UsernameClaim: testUsernameClaim, GroupsClaim: testGroupsClaim, AllowPasswordGrant: false, @@ -671,7 +723,6 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ - DoNotRequestOfflineAccess: true, AdditionalScopes: testAdditionalScopes, AdditionalAuthorizeParameters: testAdditionalParams, AllowPasswordGrant: true, @@ -700,7 +751,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Name: testName, ClientID: testClientID, AuthorizationURL: *testIssuerAuthorizeURL, - Scopes: testExpectedScopes, // does not include offline_access + Scopes: testExpectedScopes, // does not include the default scopes UsernameClaim: testUsernameClaim, GroupsClaim: testGroupsClaim, AllowPasswordGrant: true, @@ -737,6 +788,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana {Name: "code_challenge", Value: "foo"}, {Name: "code_challenge_method", Value: "foo"}, {Name: "redirect_uri", Value: "foo"}, + {Name: "hd", Value: "foo"}, {Name: "this_one_is_allowed", Value: "foo"}, }, }, @@ -751,8 +803,8 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri" "reason"="DisallowedParameterName" "status"="False" "type"="AdditionalAuthorizeParametersValid"`, - `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri" "name"="test-name" "namespace"="test-namespace" "reason"="DisallowedParameterName" "type"="AdditionalAuthorizeParametersValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "reason"="DisallowedParameterName" "status"="False" "type"="AdditionalAuthorizeParametersValid"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "name"="test-name" "namespace"="test-namespace" "reason"="DisallowedParameterName" "type"="AdditionalAuthorizeParametersValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -762,7 +814,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []v1alpha1.Condition{ {Type: "AdditionalAuthorizeParametersValid", Status: "False", LastTransitionTime: now, Reason: "DisallowedParameterName", Message: "the following additionalAuthorizeParameters are not allowed: " + - "response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri", ObservedGeneration: 1234}, + "response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd", ObservedGeneration: 1234}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, @@ -770,11 +822,11 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, }, { - name: "issuer is invalid URL, missing trailing slash", + name: "issuer is invalid URL, missing trailing slash when the OIDC discovery endpoint returns the URL with a trailing slash", inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName}, Spec: v1alpha1.OIDCIdentityProviderSpec{ - Issuer: testIssuerURL + "/ends-with-slash", + Issuer: testIssuerURL + "/ends-with-slash", // this does not end with slash when it should, thus this is an error case TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64}, Client: v1alpha1.OIDCClient{SecretName: testSecretName}, }, From d3ade82f3f3d5911a0a7c1c6e4aadadf6556d6c4 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Oct 2021 09:48:40 -0700 Subject: [PATCH 10/19] Update docs --- site/content/docs/howto/configure-supervisor-with-dex.md | 2 +- site/content/docs/howto/configure-supervisor-with-gitlab.md | 6 ++++-- site/content/docs/howto/configure-supervisor-with-okta.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/site/content/docs/howto/configure-supervisor-with-dex.md b/site/content/docs/howto/configure-supervisor-with-dex.md index 723be7cf..79a5ea35 100644 --- a/site/content/docs/howto/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/configure-supervisor-with-dex.md @@ -91,7 +91,7 @@ spec: # Request any scopes other than "openid" for claims besides # the default claims in your token. The "openid" scope is always # included. - additionalScopes: [groups, email] + additionalScopes: [offline_access, groups, email] # If you would also like to allow your end users to authenticate using # a password grant, then change this to true. diff --git a/site/content/docs/howto/configure-supervisor-with-gitlab.md b/site/content/docs/howto/configure-supervisor-with-gitlab.md index eb372dec..97058c16 100644 --- a/site/content/docs/howto/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/configure-supervisor-with-gitlab.md @@ -63,8 +63,10 @@ spec: # GitLab is unusual among OIDC providers in that it returns an # error if you request the "offline_access" scope during an # authorization flow, so ask Pinniped to avoid requesting that - # scope when using GitLab. - doNotRequestOfflineAccess: true + # scope when using GitLab by excluding it from this list. + # By specifying only "openid" here then Pinniped will only + # request "openid". + additionalScopes: [openid] # If you would also like to allow your end users to authenticate using # a password grant, then change this to true. diff --git a/site/content/docs/howto/configure-supervisor-with-okta.md b/site/content/docs/howto/configure-supervisor-with-okta.md index 0890fa2f..62110444 100644 --- a/site/content/docs/howto/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/configure-supervisor-with-okta.md @@ -74,7 +74,7 @@ spec: # # To learn more about how to customize the claims returned, see here: # https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/overview/ - additionalScopes: [groups, email] + additionalScopes: [offline_access, groups, email] # If you would also like to allow your end users to authenticate using # a password grant, then change this to true. Password grants only work From 7ec0304472359ffde5743f82938d5d78248d813f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Oct 2021 12:25:51 -0700 Subject: [PATCH 11/19] Add offline_access scope for integration tests when using Dex --- hack/prepare-for-integration-tests.sh | 2 +- test/integration/formposthtml_test.go | 8 ++++++++ test/testlib/env.go | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 90faf5bd..cce57ef5 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -372,7 +372,7 @@ export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com export PINNIPED_TEST_CLI_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" -export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES=email +export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES="offline_access,email" export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM=groups export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID=pinniped-supervisor diff --git a/test/integration/formposthtml_test.go b/test/integration/formposthtml_test.go index bfc900df..d845a60c 100644 --- a/test/integration/formposthtml_test.go +++ b/test/integration/formposthtml_test.go @@ -105,6 +105,7 @@ func TestFormPostHTML_Parallel(t *testing.T) { // // The test server supports special `?fail=close` and `?fail=500` to force error cases. func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values)) { + t.Helper() results := make(chan url.Values) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -155,6 +156,7 @@ func formpostCallbackServer(t *testing.T) (string, func(*testing.T, url.Values)) // formpostTemplateServer runs a test server that serves formposthtml.Template() rendered with test parameters. func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url.Values) string { + t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fosite.WriteAuthorizeFormPostResponse(redirectURI, responseParams, formposthtml.Template(), w) }) @@ -168,6 +170,7 @@ func formpostTemplateServer(t *testing.T, redirectURI string, responseParams url // formpostRandomParams is a helper to generate random OAuth2 response parameters for testing. func formpostRandomParams(t *testing.T) url.Values { + t.Helper() generator := &hmac.HMACStrategy{GlobalSecret: testlib.RandBytes(t, 32), TokenEntropy: 32} authCode, _, err := generator.Generate() require.NoError(t, err) @@ -180,6 +183,7 @@ func formpostRandomParams(t *testing.T) url.Values { // formpostExpectTitle asserts that the page has the expected title. func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) { + t.Helper() actual, err := page.Title() require.NoError(t, err) require.Equal(t, expected, actual) @@ -187,6 +191,7 @@ func formpostExpectTitle(t *testing.T, page *agouti.Page, expected string) { // formpostExpectTitle asserts that the page has the expected SVG/emoji favicon. func formpostExpectFavicon(t *testing.T, page *agouti.Page, expected string) { + t.Helper() iconURL, err := page.First("#favicon").Attribute("href") require.NoError(t, err) require.True(t, strings.HasPrefix(iconURL, "data:image/svg+xml, Date: Wed, 20 Oct 2021 15:53:25 -0700 Subject: [PATCH 12/19] Lots of small updates based on PR feedback --- .../types_oidcidentityprovider.go.tmpl | 5 +- ...or.pinniped.dev_oidcidentityproviders.yaml | 56 ++++++++++--------- generated/1.17/README.adoc | 2 +- .../v1alpha1/types_oidcidentityprovider.go | 5 +- ...or.pinniped.dev_oidcidentityproviders.yaml | 56 ++++++++++--------- generated/1.18/README.adoc | 2 +- .../v1alpha1/types_oidcidentityprovider.go | 5 +- ...or.pinniped.dev_oidcidentityproviders.yaml | 56 ++++++++++--------- generated/1.19/README.adoc | 2 +- .../v1alpha1/types_oidcidentityprovider.go | 5 +- ...or.pinniped.dev_oidcidentityproviders.yaml | 56 ++++++++++--------- generated/1.20/README.adoc | 2 +- .../v1alpha1/types_oidcidentityprovider.go | 5 +- ...or.pinniped.dev_oidcidentityproviders.yaml | 56 ++++++++++--------- .../v1alpha1/types_oidcidentityprovider.go | 5 +- .../oidc_upstream_watcher.go | 19 +++---- .../fositestorage/accesstoken/accesstoken.go | 2 + .../authorizationcode/authorizationcode.go | 2 + .../openidconnect/openidconnect.go | 2 + internal/fositestorage/pkce/pkce.go | 2 + .../refreshtoken/refreshtoken.go | 2 + internal/oidc/auth/auth_handler.go | 3 +- internal/oidc/callback/callback_handler.go | 3 +- .../provider/dynamic_upstream_idp_provider.go | 2 +- pkg/oidcclient/login.go | 1 + pkg/oidcclient/login_test.go | 1 + .../howto/configure-supervisor-with-gitlab.md | 7 ++- .../howto/configure-supervisor-with-okta.md | 4 +- 28 files changed, 208 insertions(+), 160 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl index bae993d0..a53d6f53 100644 --- a/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 14f6fd89..8ebd5eb5 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -114,32 +114,36 @@ spec: Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. By setting - this list to anything other than an empty list, you are overriding - the default value, so you may wish to include some of "offline_access", - "email", and "profile" in your override list. Some OIDC providers - may also require a scope to get access to the user''s group - membership, in which case you may wish to include it in this - list. Sometimes the scope to request the user''s group membership - is called "groups", but unfortunately this is not specified - in the OIDC standard. Generally speaking, you should include - any scopes required to cause the appropriate claims to be the - returned by your OIDC provider in the ID token or userinfo endpoint - results for those claims which you would like to use in the - oidcClaims settings to determine the usernames and group memberships - of your Kubernetes users. See your OIDC provider''s documentation - for more information about what scopes are available to request - claims. Additionally, the Pinniped Supervisor requires that - your OIDC provider returns refresh tokens to the Supervisor - from these authorization flows. For most OIDC providers, the - scope required to receive refresh tokens will be "offline_access". - See the documentation of your OIDC provider''s authorization - and token endpoints for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. Note that it may be safe to send "offline_access" - even to providers which do not require it, since the provider - may ignore scopes that it does not understand or require (see - https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 35b16323..d18e7610 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -1099,7 +1099,7 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index bae993d0..a53d6f53 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 14f6fd89..8ebd5eb5 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -114,32 +114,36 @@ spec: Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. By setting - this list to anything other than an empty list, you are overriding - the default value, so you may wish to include some of "offline_access", - "email", and "profile" in your override list. Some OIDC providers - may also require a scope to get access to the user''s group - membership, in which case you may wish to include it in this - list. Sometimes the scope to request the user''s group membership - is called "groups", but unfortunately this is not specified - in the OIDC standard. Generally speaking, you should include - any scopes required to cause the appropriate claims to be the - returned by your OIDC provider in the ID token or userinfo endpoint - results for those claims which you would like to use in the - oidcClaims settings to determine the usernames and group memberships - of your Kubernetes users. See your OIDC provider''s documentation - for more information about what scopes are available to request - claims. Additionally, the Pinniped Supervisor requires that - your OIDC provider returns refresh tokens to the Supervisor - from these authorization flows. For most OIDC providers, the - scope required to receive refresh tokens will be "offline_access". - See the documentation of your OIDC provider''s authorization - and token endpoints for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. Note that it may be safe to send "offline_access" - even to providers which do not require it, since the provider - may ignore scopes that it does not understand or require (see - https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 2f383af3..417b57ac 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -1099,7 +1099,7 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index bae993d0..a53d6f53 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 14f6fd89..8ebd5eb5 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -114,32 +114,36 @@ spec: Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. By setting - this list to anything other than an empty list, you are overriding - the default value, so you may wish to include some of "offline_access", - "email", and "profile" in your override list. Some OIDC providers - may also require a scope to get access to the user''s group - membership, in which case you may wish to include it in this - list. Sometimes the scope to request the user''s group membership - is called "groups", but unfortunately this is not specified - in the OIDC standard. Generally speaking, you should include - any scopes required to cause the appropriate claims to be the - returned by your OIDC provider in the ID token or userinfo endpoint - results for those claims which you would like to use in the - oidcClaims settings to determine the usernames and group memberships - of your Kubernetes users. See your OIDC provider''s documentation - for more information about what scopes are available to request - claims. Additionally, the Pinniped Supervisor requires that - your OIDC provider returns refresh tokens to the Supervisor - from these authorization flows. For most OIDC providers, the - scope required to receive refresh tokens will be "offline_access". - See the documentation of your OIDC provider''s authorization - and token endpoints for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. Note that it may be safe to send "offline_access" - even to providers which do not require it, since the provider - may ignore scopes that it does not understand or require (see - https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 0dace9e2..0317faed 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -1099,7 +1099,7 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index bae993d0..a53d6f53 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 14f6fd89..8ebd5eb5 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -114,32 +114,36 @@ spec: Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. By setting - this list to anything other than an empty list, you are overriding - the default value, so you may wish to include some of "offline_access", - "email", and "profile" in your override list. Some OIDC providers - may also require a scope to get access to the user''s group - membership, in which case you may wish to include it in this - list. Sometimes the scope to request the user''s group membership - is called "groups", but unfortunately this is not specified - in the OIDC standard. Generally speaking, you should include - any scopes required to cause the appropriate claims to be the - returned by your OIDC provider in the ID token or userinfo endpoint - results for those claims which you would like to use in the - oidcClaims settings to determine the usernames and group memberships - of your Kubernetes users. See your OIDC provider''s documentation - for more information about what scopes are available to request - claims. Additionally, the Pinniped Supervisor requires that - your OIDC provider returns refresh tokens to the Supervisor - from these authorization flows. For most OIDC providers, the - scope required to receive refresh tokens will be "offline_access". - See the documentation of your OIDC provider''s authorization - and token endpoints for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. Note that it may be safe to send "offline_access" - even to providers which do not require it, since the provider - may ignore scopes that it does not understand or require (see - https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index cbdcb6fd..faf7ad54 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -1099,7 +1099,7 @@ OIDCAuthorizationConfig provides information about how to form the OAuth2 author [cols="25a,75a", options="header"] |=== | Field | Description -| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). | *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. | *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index bae993d0..a53d6f53 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 14f6fd89..8ebd5eb5 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -114,32 +114,36 @@ spec: Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - for a description of the "offline_access" scope. By setting - this list to anything other than an empty list, you are overriding - the default value, so you may wish to include some of "offline_access", - "email", and "profile" in your override list. Some OIDC providers - may also require a scope to get access to the user''s group - membership, in which case you may wish to include it in this - list. Sometimes the scope to request the user''s group membership - is called "groups", but unfortunately this is not specified - in the OIDC standard. Generally speaking, you should include - any scopes required to cause the appropriate claims to be the - returned by your OIDC provider in the ID token or userinfo endpoint - results for those claims which you would like to use in the - oidcClaims settings to determine the usernames and group memberships - of your Kubernetes users. See your OIDC provider''s documentation - for more information about what scopes are available to request - claims. Additionally, the Pinniped Supervisor requires that - your OIDC provider returns refresh tokens to the Supervisor - from these authorization flows. For most OIDC providers, the - scope required to receive refresh tokens will be "offline_access". - See the documentation of your OIDC provider''s authorization - and token endpoints for its requirements for what to include - in the request in order to receive a refresh token in the response, - if anything. Note that it may be safe to send "offline_access" - even to providers which do not require it, since the provider - may ignore scopes that it does not understand or require (see - https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go index bae993d0..a53d6f53 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -45,8 +45,11 @@ type OIDCAuthorizationConfig struct { // the following scopes: "openid", "offline_access", "email", and "profile". See // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the - // "offline_access" scope. By setting this list to anything other than an empty list, you are overriding the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you // may wish to include it in this list. Sometimes the scope to request the user's group membership is called // "groups", but unfortunately this is not specified in the OIDC standard. diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 9719be44..34e81631 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -12,10 +12,11 @@ import ( "fmt" "net/http" "net/url" - "sort" "strings" "time" + "k8s.io/apimachinery/pkg/util/sets" + "github.com/coreos/go-oidc/v3/oidc" "github.com/go-logr/logr" "golang.org/x/oauth2" @@ -427,20 +428,14 @@ func computeScopes(additionalScopes []string) []string { } // Otherwise, first compute the unique set of scopes, including "openid" (de-duplicate). - set := make(map[string]bool, len(additionalScopes)+1) - set["openid"] = true + set := sets.NewString() + set.Insert("openid") for _, s := range additionalScopes { - set[s] = true + set.Insert(s) } - // Then grab all the keys and sort them. - scopes := make([]string, 0, len(set)) - for s := range set { - scopes = append(scopes, s) - } - sort.Strings(scopes) - - return scopes + // Return the set as a sorted list. + return set.List() } func truncateMostLongErr(err error) string { diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index fc2427ad..dbd57fb3 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -26,6 +26,8 @@ const ( ErrInvalidAccessTokenRequestVersion = constable.Error("access token request data has wrong version") ErrInvalidAccessTokenRequestData = constable.Error("access token request data must be present") + // Version 1 was the initial release of storage. + // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. accessTokenStorageVersion = "2" ) diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index a76f684f..b259e406 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -27,6 +27,8 @@ const ( ErrInvalidAuthorizeRequestData = constable.Error("authorization request data must be present") ErrInvalidAuthorizeRequestVersion = constable.Error("authorization request data has wrong version") + // Version 1 was the initial release of storage. + // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. authorizeCodeStorageVersion = "2" ) diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index f1e2200f..81699410 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -28,6 +28,8 @@ const ( ErrInvalidOIDCRequestData = constable.Error("oidc request data must be present") ErrMalformedAuthorizationCode = constable.Error("malformed authorization code") + // Version 1 was the initial release of storage. + // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. oidcStorageVersion = "2" ) diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index cf6cec69..cbe566bd 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -26,6 +26,8 @@ const ( ErrInvalidPKCERequestVersion = constable.Error("pkce request data has wrong version") ErrInvalidPKCERequestData = constable.Error("pkce request data must be present") + // Version 1 was the initial release of storage. + // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. pkceStorageVersion = "2" ) diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index a456ed75..c2d4336c 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -26,6 +26,8 @@ const ( ErrInvalidRefreshTokenRequestVersion = constable.Error("refresh token request data has wrong version") ErrInvalidRefreshTokenRequestData = constable.Error("refresh token request data must be present") + // Version 1 was the initial release of storage. + // Version 2 is when we switched to storing psession.PinnipedSession inside the fosite request. refreshTokenStorageVersion = "2" ) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 270678bc..4c457faf 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -160,7 +160,8 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( } if token.RefreshToken == nil || token.RefreshToken.Token == "" { - plog.Warning("refresh token not returned by upstream provider during password grant", + plog.Warning("refresh token not returned by upstream provider during password grant, "+ + "please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI", "upstreamName", oidcUpstream.GetName(), "scopes", oidcUpstream.GetScopes()) return writeAuthorizeError(w, oauthHelper, authorizeRequester, diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index e99d1678..22f37a55 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -70,7 +70,8 @@ func NewHandler( } if token.RefreshToken == nil || token.RefreshToken.Token == "" { - plog.Warning("refresh token not returned by upstream provider during authcode exchange", + plog.Warning("refresh token not returned by upstream provider during authcode exchange, "+ + "please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI", "upstreamName", upstreamIDPConfig.GetName(), "scopes", upstreamIDPConfig.GetScopes(), "additionalParams", upstreamIDPConfig.GetAdditionalAuthcodeParams()) diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index 9ece5f4d..88710f00 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -64,7 +64,7 @@ type UpstreamOIDCIdentityProviderI interface { ) (*oidctypes.Token, error) // PerformRefresh will call the provider's token endpoint to perform a refresh grant. The provider may or may not - // return a new ID or refresh token in the response. If it returns an ID token, then use ValidateRefresh to + // return a new ID or refresh token in the response. If it returns an ID token, then use ValidateToken to // validate the ID token. PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 3d62e05d..46319490 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -813,6 +813,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) if err != nil { // Ignore errors during refresh, but return nil which will trigger the full login flow. + h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.") return nil, nil } diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index 2c30ffb0..e2548ebd 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -513,6 +513,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo wantLogs: []string{ `"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`, `"level"=4 "msg"="Pinniped: Refreshing cached token."`, + `"level"=4 "msg"="Pinniped: Refresh failed."`, `"msg"="could not open callback listener" "error"="some listen error"`, }, // Expect this to fall through to the authorization code flow, so it fails here. diff --git a/site/content/docs/howto/configure-supervisor-with-gitlab.md b/site/content/docs/howto/configure-supervisor-with-gitlab.md index 97058c16..c7998470 100644 --- a/site/content/docs/howto/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/configure-supervisor-with-gitlab.md @@ -32,8 +32,7 @@ For example, to create a user-owned application: 1. Enter the redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. 1. Check the box saying that the application is _Confidential_. This is required and will cause GitLab to autogenerate a client ID and client secret for your application. - 1. Check the box saying to _Expire Access Tokens_ to cause refresh tokens to be returned to the Supervisor. This is - required starting in Pinniped v0.13.0. + 1. Check the box saying to _Expire Access Tokens_ to cause refresh tokens to be returned to the Supervisor. 1. Select scope `openid`. This is required to get ID tokens. Also, this provides access to the `nickname` (GitLab username) and `groups` (GitLab groups) claims in the ID tokens. 1. Optionally select other scopes which might provide access to other claims that you might want to use to determine @@ -69,7 +68,9 @@ spec: additionalScopes: [openid] # If you would also like to allow your end users to authenticate using - # a password grant, then change this to true. + # a password grant, then change this to true. See + # https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow + # for more information about using the password grant with GitLab. allowPasswordGrant: false # Specify how GitLab claims are mapped to Kubernetes identities. diff --git a/site/content/docs/howto/configure-supervisor-with-okta.md b/site/content/docs/howto/configure-supervisor-with-okta.md index 62110444..d90c7d82 100644 --- a/site/content/docs/howto/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/configure-supervisor-with-okta.md @@ -36,9 +36,9 @@ For example, to create an app: password grant flow to your end users, then choose `Native Application` instead. 1. Enter a name for your app, such as "My Kubernetes Clusters". 1. If you chose to create a `Web Application` then in the General Settings section, choose Grant Types - `Authorization Code` and `Refresh Token`. Starting in Pinniped v0.13.0, the `Refresh Token` grant is required. + `Authorization Code` and `Refresh Token`. 1. If you chose `Native Application` then in the General Settings section, choose Grant Types `Authorization Code`, - `Refresh Token`, and `Resource Owner Password`. Starting in Pinniped v0.13.0, the `Refresh Token` grant is required. + `Refresh Token`, and `Resource Owner Password`. 1. Enter the sign-in redirect URI. This is the `spec.issuer` you configured in your `FederationDomain` appended with `/callback`. 1. Optionally select `Limit access to selected groups` to restrict which Okta users can log in to Kubernetes using this integration. 1. Save the app and make note of the _Client ID_ and _Client secret_. If you chose to create a `Native Application` From b3a1dcd634e8088a60c7e3f671716f54e6013e70 Mon Sep 17 00:00:00 2001 From: anjalitelang <49958114+anjaltelang@users.noreply.github.com> Date: Thu, 21 Oct 2021 10:10:19 -0400 Subject: [PATCH 13/19] Update ROADMAP.md Updated roadmap to reflect current focus of Pinniped project --- ROADMAP.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 12922864..b406e56f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,6 +38,21 @@ Last Updated: Sept 2021 |--|--|--| |Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for OIDC |Oct 2021| |Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for LDAP/AD |Nov 2021| +|Improving Security Posture|Set stricter default TLS versions and Ciphers |Nov 2021| +|Improving Security Posture|Support FIPS compliant Boring crypto libraries |Dec 2021| +|Improving Security Posture|Support Audit logging of security events related to Authentication |Jan 2022| +|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| +|Improving Security Posture|Key management/rotation for Pinniped components with minimal downtime |Exploring/Ongoing| +|Improving Security Posture|Support for Session Logout |Exploring/Ongoing| +|Improving Security Posture|Support for Idle Session/ Inactivity timeout|Exploring/Ongoing| +|Improving Security Posture|Support for Max Concurrent Sessions|Exploring/Ongoing| +|Improving Security Posture|Support for configurable Session Length |Exploring/Ongoing| +|Improving Security Posture|Reject use of username and groups with system: prefix |Exploring/Ongoing| +|Improving Security Posture|Support for using external KMS for Supervisor signing keys |Exploring/Ongoing| +|Improving Security Posture|Client side use of Secure Enclaves for Session data |Exploring/Ongoing| +|Improving Security Posture|Enforce the use of HTTP Strict Transport (HSTS) |Exploring/Ongoing| +|Improving Security Posture|Assert that Pinniped runs under the restricted PSP version2 levels |Exploring/Ongoing| +|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| |Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing| |Wider Concierge cluster support|Support for OpenShift cluster types in the Concierge|Exploring/Ongoing| |Identity transforms|Support prefixing, filtering, or performing coarse-grained checks on upstream users and groups|Exploring/Ongoing| From be6c335bb820c1846ef568c6da4064c1eaf7df41 Mon Sep 17 00:00:00 2001 From: anjalitelang <49958114+anjaltelang@users.noreply.github.com> Date: Thu, 21 Oct 2021 10:16:54 -0400 Subject: [PATCH 14/19] Update ROADMAP.md Minor changes --- ROADMAP.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b406e56f..882838c4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -37,7 +37,7 @@ Last Updated: Sept 2021 |Theme|Description|Timeline| |--|--|--| |Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for OIDC |Oct 2021| -|Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for LDAP/AD |Nov 2021| +|Improving Security Posture|Supervisor token refresh fails when the upstream user is in an invalid state for LDAP/AD |Nov 2021| |Improving Security Posture|Set stricter default TLS versions and Ciphers |Nov 2021| |Improving Security Posture|Support FIPS compliant Boring crypto libraries |Dec 2021| |Improving Security Posture|Support Audit logging of security events related to Authentication |Jan 2022| @@ -52,7 +52,6 @@ Last Updated: Sept 2021 |Improving Security Posture|Client side use of Secure Enclaves for Session data |Exploring/Ongoing| |Improving Security Posture|Enforce the use of HTTP Strict Transport (HSTS) |Exploring/Ongoing| |Improving Security Posture|Assert that Pinniped runs under the restricted PSP version2 levels |Exploring/Ongoing| -|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| |Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing| |Wider Concierge cluster support|Support for OpenShift cluster types in the Concierge|Exploring/Ongoing| |Identity transforms|Support prefixing, filtering, or performing coarse-grained checks on upstream users and groups|Exploring/Ongoing| From e0db59fd090486e90a44880c2b9d9549fe00d3a7 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 22 Oct 2021 10:23:21 -0700 Subject: [PATCH 15/19] More small updates based on PR feedback --- .../oidc_upstream_watcher.go | 9 +++- .../oidc_upstream_watcher_test.go | 43 +++++++++++++++++++ internal/oidc/token/token_handler.go | 5 +-- internal/oidc/token/token_handler_test.go | 2 +- pkg/oidcclient/login.go | 2 +- 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 34e81631..0e4399ea 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -60,6 +60,7 @@ const ( reasonUnreachable = "Unreachable" reasonInvalidResponse = "InvalidResponse" reasonDisallowedParameterName = "DisallowedParameterName" + allParamNamesAllowedMsg = "additionalAuthorizeParameters parameter names are allowed" // Errors that are generated by our reconcile process. errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") @@ -221,7 +222,6 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst c.validateIssuer(ctx.Context, upstream, &result), } if len(rejectedAuthcodeAuthorizeParameters) > 0 { - // This condition probably isn't important enough to report when it is successful, so just report errors. conditions = append(conditions, &v1alpha1.Condition{ Type: typeAdditionalAuthorizeParametersValid, Status: v1alpha1.ConditionFalse, @@ -229,6 +229,13 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst Message: fmt.Sprintf("the following additionalAuthorizeParameters are not allowed: %s", strings.Join(rejectedAuthcodeAuthorizeParameters, ",")), }) + } else { + conditions = append(conditions, &v1alpha1.Condition{ + Type: typeAdditionalAuthorizeParametersValid, + Status: v1alpha1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: allParamNamesAllowedMsg, + }) } c.updateStatus(ctx.Context, upstream, conditions) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index dc61c63d..57942c02 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -119,6 +119,16 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { require.NoError(t, err) wrongCABase64 := base64.StdEncoding.EncodeToString(wrongCA.Bundle()) + happyAdditionalAuthorizeParametersValidCondition := v1alpha1.Condition{ + Type: "AdditionalAuthorizeParametersValid", + Status: "True", + Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", + LastTransitionTime: now, + } + happyAdditionalAuthorizeParametersValidConditionEarlier := happyAdditionalAuthorizeParametersValidCondition + happyAdditionalAuthorizeParametersValidConditionEarlier.LastTransitionTime = earlier + var ( testNamespace = "test-namespace" testName = "test-name" @@ -162,6 +172,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -170,6 +181,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "False", @@ -207,6 +219,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -215,6 +228,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "False", @@ -251,6 +265,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -259,6 +274,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "False", @@ -298,6 +314,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -306,6 +323,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -345,6 +363,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -353,6 +372,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -390,6 +410,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -398,6 +419,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -437,6 +459,7 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "issuer"="` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": x509: certificate signed by unknown authority" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -445,6 +468,7 @@ Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-na Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -483,6 +507,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -491,6 +516,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -528,6 +554,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -536,6 +563,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -584,6 +612,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -603,6 +632,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"}, }, @@ -622,6 +652,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidConditionEarlier, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, @@ -635,6 +666,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -654,6 +686,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, @@ -676,6 +709,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidConditionEarlier, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, @@ -689,6 +723,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -708,6 +743,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, @@ -732,6 +768,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidConditionEarlier, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, @@ -745,6 +782,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantLogs: []string{ `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -764,6 +802,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, @@ -841,6 +880,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -849,6 +889,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", @@ -888,6 +929,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, @@ -896,6 +938,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs Status: v1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + happyAdditionalAuthorizeParametersValidCondition, { Type: "ClientCredentialsValid", Status: "True", diff --git a/internal/oidc/token/token_handler.go b/internal/oidc/token/token_handler.go index 724ee0aa..30956524 100644 --- a/internal/oidc/token/token_handler.go +++ b/internal/oidc/token/token_handler.go @@ -156,9 +156,8 @@ func findOIDCProviderByNameAndValidateUID( for _, p := range providerCache.GetOIDCIdentityProviders() { if p.GetName() == s.ProviderName { if p.GetResourceUID() != s.ProviderUID { - return nil, errorsx.WithStack(errUpstreamRefreshError.WithHintf( - "Provider %q of type %q from upstream session data has changed its resource UID since authentication.", - s.ProviderName, s.ProviderType)) + return nil, errorsx.WithStack(errUpstreamRefreshError.WithHint( + "Provider from upstream session data has changed its resource UID since authentication.")) } return p, nil } diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 968944d7..3b6be1ff 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -1432,7 +1432,7 @@ func TestRefreshGrant(t *testing.T) { wantErrorResponseBody: here.Doc(` { "error": "error", - "error_description": "Error during upstream refresh. Provider 'some-oidc-idp' of type 'oidc' from upstream session data has changed its resource UID since authentication." + "error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication." } `), }, diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 46319490..fad28e10 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -813,7 +813,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) if err != nil { // Ignore errors during refresh, but return nil which will trigger the full login flow. - h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.") + h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "err", err) return nil, nil } From 303b1f07d39df829de088ea32701c8a873c68739 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 22 Oct 2021 14:06:31 -0700 Subject: [PATCH 16/19] Fix mistake in previous commit --- pkg/oidcclient/login.go | 2 +- pkg/oidcclient/login_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index fad28e10..b01e5238 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -813,7 +813,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) if err != nil { // Ignore errors during refresh, but return nil which will trigger the full login flow. - h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "err", err) + h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "error", err.Error()) return nil, nil } diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index e2548ebd..cdb497e6 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -513,7 +513,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo wantLogs: []string{ `"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`, `"level"=4 "msg"="Pinniped: Refreshing cached token."`, - `"level"=4 "msg"="Pinniped: Refresh failed."`, + `"level"=4 "msg"="Pinniped: Refresh failed." "error"="oauth2: cannot fetch token: 400 Bad Request\nResponse: expected client_id 'test-client-id'\n"`, `"msg"="could not open callback listener" "error"="some listen error"`, }, // Expect this to fall through to the authorization code flow, so it fails here. From 1e17418585af2e8f1020ebbe39a9cda2a15edf9c Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Mon, 25 Oct 2021 10:21:51 -0400 Subject: [PATCH 17/19] TestSupervisorUpstreamOIDCDiscovery: include AdditionalAuthorizeParametersValid condition Signed-off-by: Monis Khan --- test/integration/supervisor_upstream_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/integration/supervisor_upstream_test.go b/test/integration/supervisor_upstream_test.go index 24e316ae..7d3164a5 100644 --- a/test/integration/supervisor_upstream_test.go +++ b/test/integration/supervisor_upstream_test.go @@ -40,6 +40,12 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) { Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": dial tcp: address 444444: in [truncated 10 chars]`, }, + { + Type: "AdditionalAuthorizeParametersValid", + Status: "True", + Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", + }, }) }) @@ -72,6 +78,12 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan Message: `failed to perform OIDC discovery against "` + env.SupervisorUpstreamOIDC.Issuer + `/": oidc: issuer did not match the issuer returned by provider, expected "` + env.SupervisorUpstreamOIDC.Issuer + `/" got "` + env.SupervisorUpstreamOIDC.Issuer + `"`, }, + { + Type: "AdditionalAuthorizeParametersValid", + Status: "True", + Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", + }, }) }) @@ -103,6 +115,12 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su Reason: "Success", Message: "discovered issuer configuration", }, + { + Type: "AdditionalAuthorizeParametersValid", + Status: "True", + Reason: "Success", + Message: "additionalAuthorizeParameters parameter names are allowed", + }, }) }) } From 7d30bfc22c8184071d6c3c1d902b6a85b8aac140 Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Mon, 25 Oct 2021 16:05:12 -0400 Subject: [PATCH 18/19] Start using CodeQL --- .github/workflows/codeql-analysis.yml | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..6ae22f29 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,57 @@ +name: "CodeQL" + +on: + push: + branches: [ main, release* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, release* ] + schedule: + - cron: '39 13 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'javascript' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 7921a5898857883b2d8c8edb7d111a022d881140 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Mon, 25 Oct 2021 16:21:54 -0400 Subject: [PATCH 19/19] Use 65532 instead of 1001 as non-root user Signed-off-by: Monis Khan --- Dockerfile | 4 +++- deploy/concierge/values.yaml | 4 ++-- deploy/local-user-authenticator/values.yaml | 4 ++-- deploy/supervisor/values.yaml | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4a19049e..13e915b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,9 @@ COPY --from=build-env /usr/local/bin /usr/local/bin EXPOSE 8080 8443 # Run as non-root for security posture -USER 1001:1001 +# Use the same non-root user as https://github.com/GoogleContainerTools/distroless/blob/fc3c4eaceb0518900f886aae90407c43be0a42d9/base/base.bzl#L9 +# This is a workaround for https://github.com/GoogleContainerTools/distroless/issues/718 +USER 65532:65532 # Set the entrypoint ENTRYPOINT ["/usr/local/bin/pinniped-server"] diff --git a/deploy/concierge/values.yaml b/deploy/concierge/values.yaml index a0e0750c..76d77a64 100644 --- a/deploy/concierge/values.yaml +++ b/deploy/concierge/values.yaml @@ -55,8 +55,8 @@ api_serving_certificate_renew_before_seconds: 2160000 #! information), trace (timing information), all (kitchen sink). log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. -run_as_user: 1001 #! run_as_user specifies the user ID that will own the process -run_as_group: 1001 #! run_as_group specifies the group ID that will own the process +run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice +run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice #! Specify the API group suffix for all Pinniped API groups. By default, this is set to #! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev, diff --git a/deploy/local-user-authenticator/values.yaml b/deploy/local-user-authenticator/values.yaml index f39cb42d..1f65baa4 100644 --- a/deploy/local-user-authenticator/values.yaml +++ b/deploy/local-user-authenticator/values.yaml @@ -15,5 +15,5 @@ image_tag: latest #! Optional. image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}} -run_as_user: 1001 #! run_as_user specifies the user ID that will own the process -run_as_group: 1001 #! run_as_group specifies the group ID that will own the process +run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice +run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice diff --git a/deploy/supervisor/values.yaml b/deploy/supervisor/values.yaml index ea3fa2a4..16b036b1 100644 --- a/deploy/supervisor/values.yaml +++ b/deploy/supervisor/values.yaml @@ -57,8 +57,8 @@ service_loadbalancer_ip: #! e.g. 1.2.3.4 #! information), trace (timing information), all (kitchen sink). log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. -run_as_user: 1001 #! run_as_user specifies the user ID that will own the process -run_as_group: 1001 #! run_as_group specifies the group ID that will own the process +run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice +run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice #! Specify the API group suffix for all Pinniped API groups. By default, this is set to #! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,