Supervisor should emit a warning when access token lifetime is too short

This commit is contained in:
Margo Crawford 2022-01-18 15:34:19 -08:00
parent 284ce00aef
commit b0ea7063c7
13 changed files with 130 additions and 32 deletions

View File

@ -53,7 +53,7 @@ func TestAccessTokenStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",
@ -122,7 +122,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/access-token", Type: "storage.pinniped.dev/access-token",

View File

@ -369,36 +369,41 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
"providerUID": "Ĝ眧Ĭ", "providerUID": "Ĝ眧Ĭ",
"providerName": "ʼn2ƋŢ觛ǂ焺nŐǛ", "providerName": "ʼn2ƋŢ觛ǂ焺nŐǛ",
"providerType": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4", "providerType": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4",
"warnings": [
"掘ʃƸ澺淗a紽ǒ|鰽ŋ猊",
"毇妬\u003e6鉢緋uƴŤȱʀļÂ?"
],
"oidc": { "oidc": {
"upstreamRefreshToken": "tC嵽痊w", "upstreamRefreshToken": "\u003cƬb",
"upstreamAccessToken": "a紽ǒ|鰽ŋ猊I", "upstreamAccessToken": "犘c钡ɏȫ",
"upstreamSubject": "妬\u003e6鉢緋uƴŤȱʀ", "upstreamSubject": "",
"upstreamIssuer": ":設虝27就伒犘c" "upstreamIssuer": "%OpKȱ藚ɏ¬Ê蒭堜"
}, },
"ldap": { "ldap": {
"userDN": "ɏȫ齁š%Op", "userDN": "ȗ韚ʫ繕ȫ碰+",
"extraRefreshAttributes": { "extraRefreshAttributes": {
"T妼É4İ\u003e×1": "ʥ笿0D", "+î艔垎0": ",
"÷驣7Ʀ澉1æɽ誮": "ʫ繕ȫ", "4İ": "墀jMʥ",
"ŚB碠k9": "i磊ůď逳鞪?3)藵睋邔\u0026Ű" "k9": "磊ůď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ"
} }
}, },
"activedirectory": { "activedirectory": {
"userDN": "s", "userDN": "%Ä摱ìÓȐĨf跞@)¿,ɭS隑i",
"extraRefreshAttributes": { "extraRefreshAttributes": {
"ƉǢIȽ齤士bEǎ儯惝IozŁ5rƖ螼": "偶宾儮猷V麹Œ颛Ė應,Ɣ鬅X¤" " 皦pSǬŝ社Vƅȭǝ*擦28Dž": "vư",
"艱iYn面@yȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛"
} }
} }
} }
}, },
"requestedAudience": [ "requestedAudience": [
"tO灞浛a齙\\蹼偦歛ơ", "置b",
"皦pSǬŝ社Vƅȭǝ*" "筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{"
], ],
"grantedAudience": [ "grantedAudience": [
"ĝ\"zvưã置bņ抰蛖a³2ʫ", "jÃ轘屔挝",
"Ŷɽ蔒PR}Ųʓl{鼐jÃ轘屔挝", "Œų崓ļ憽-蹐È_¸]fś",
"Œų崓ļ憽-蹐È_¸]fś" "ɵʮGɃɫ囤1+,Ȳ"
] ]
}, },
"version": "2" "version": "2"

View File

@ -65,7 +65,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",
@ -84,7 +84,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/authcode", Type: "storage.pinniped.dev/authcode",

View File

@ -52,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/oidc", Type: "storage.pinniped.dev/oidc",

View File

@ -52,7 +52,7 @@ func TestPKCEStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/pkce", Type: "storage.pinniped.dev/pkce",

View File

@ -52,7 +52,7 @@ func TestRefreshTokenStorage(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",
@ -122,7 +122,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
}, },
}, },
Data: map[string][]byte{ 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":{"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","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`), "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","warnings":null,"oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
"pinniped-storage-version": []byte("1"), "pinniped-storage-version": []byte("1"),
}, },
Type: "storage.pinniped.dev/refresh-token", Type: "storage.pinniped.dev/refresh-token",

View File

@ -14,11 +14,13 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"time"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
@ -893,7 +895,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "OIDC password grant happy path when upstream IDP returned empty refresh token but it did return an access token and has a userinfo endpoint", name: "OIDC password grant happy path when upstream IDP returned empty refresh token but it did return an access token and has a userinfo endpoint",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
@ -913,9 +915,41 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken, wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken,
}, },
{
name: "OIDC password grant happy path when upstream IDP returned empty refresh token and an access token that has a short lifetime",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound,
wantContentType: htmlContentType,
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: &psession.CustomSessionData{
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
ProviderName: oidcPasswordGrantUpstreamName,
ProviderType: psession.ProviderTypeOIDC,
Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."},
OIDC: &psession.OIDCSessionData{
UpstreamAccessToken: oidcUpstreamAccessToken,
UpstreamSubject: oidcUpstreamSubject,
UpstreamIssuer: oidcUpstreamIssuer,
},
},
},
{ {
name: "OIDC password grant happy path when upstream IDP did not return a refresh token but it did return an access token and has a userinfo endpoint", name: "OIDC password grant happy path when upstream IDP did not return a refresh token but it did return an access token and has a userinfo endpoint",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
@ -1078,7 +1112,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "password grant returns an error when upstream IDP returns no refresh token with an access token but has no userinfo endpoint", name: "password grant returns an error when upstream IDP returns no refresh token with an access token but has no userinfo endpoint",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
@ -1091,7 +1125,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
}, },
{ {
name: "password grant returns an error when upstream IDP returns empty refresh token with an access token but has no userinfo endpoint", name: "password grant returns an error when upstream IDP returns empty refresh token with an access token but has no userinfo endpoint",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),

View File

@ -11,9 +11,11 @@ import (
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
"time"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -213,7 +215,7 @@ func TestCallbackEndpoint(t *testing.T) {
}, },
{ {
name: "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code", name: "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(), path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,
@ -234,6 +236,39 @@ func TestCallbackEndpoint(t *testing.T) {
args: happyExchangeAndValidateTokensArgs, args: happyExchangeAndValidateTokensArgs,
}, },
}, },
{
name: "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusSeeOther,
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
wantBody: "",
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: &psession.CustomSessionData{
ProviderUID: happyUpstreamIDPResourceUID,
ProviderName: happyUpstreamIDPName,
ProviderType: psession.ProviderTypeOIDC,
Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."},
OIDC: &psession.OIDCSessionData{
UpstreamAccessToken: oidcUpstreamAccessToken,
UpstreamIssuer: oidcUpstreamIssuer,
UpstreamSubject: oidcUpstreamSubject,
},
},
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{ {
name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
@ -358,7 +393,7 @@ func TestCallbackEndpoint(t *testing.T) {
}, },
{ {
name: "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint", name: "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()),
method: http.MethodGet, method: http.MethodGet,
path: newRequestPath().WithState(happyState).String(), path: newRequestPath().WithState(happyState).String(),
csrfCookie: happyCSRFCookie, csrfCookie: happyCSRFCookie,

View File

@ -14,6 +14,7 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -101,6 +102,13 @@ func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdent
} }
plog.Info("refresh token not returned by upstream provider during login, using access token instead. "+pleaseCheck, logKV...) plog.Info("refresh token not returned by upstream provider during login, using access token instead. "+pleaseCheck, logKV...)
customSessionData.OIDC.UpstreamAccessToken = token.AccessToken.Token customSessionData.OIDC.UpstreamAccessToken = token.AccessToken.Token
// When we are in a flow where we will be performing access token based refresh, issue a warning to the client if the access
// token lifetime is very short, since that would mean that the user's session is very short.
// The warnings are stored here and will be processed by the token handler.
threeHoursFromNow := metav1.NewTime(time.Now().Add(3 * time.Hour))
if !token.AccessToken.Expiry.IsZero() && token.AccessToken.Expiry.Before(&threeHoursFromNow) {
customSessionData.Warnings = append(customSessionData.Warnings, "Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in.")
}
default: default:
plog.Warning("refresh token and access token not returned by upstream provider during login. "+pleaseCheck, logKV...) plog.Warning("refresh token and access token not returned by upstream provider during login. "+pleaseCheck, logKV...)
return nil, errors.New("neither access token nor refresh token returned by upstream provider") return nil, errors.New("neither access token nor refresh token returned by upstream provider")

View File

@ -12,6 +12,7 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/x/errorsx" "github.com/ory/x/errorsx"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apiserver/pkg/warning"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -63,6 +64,14 @@ func NewHandler(
} }
} }
// When we are in the authorization code flow, check if we have any warnings that previous handlers want us
// to send to the client to be printed on the CLI.
if accessRequest.GetGrantTypes().ExactOne("authorization_code") {
for _, warningText := range session.Custom.Warnings {
warning.AddWarning(r.Context(), "", warningText)
}
}
accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest) accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest)
if err != nil { if err != nil {
plog.Info("token response error", oidc.FositeErrorForLog(err)...) plog.Info("token response error", oidc.FositeErrorForLog(err)...)

View File

@ -43,6 +43,10 @@ type CustomSessionData struct {
// Used during a downstream refresh to decide which upstream to refresh. // Used during a downstream refresh to decide which upstream to refresh.
ProviderType ProviderType `json:"providerType"` ProviderType ProviderType `json:"providerType"`
// Warnings that were encountered at some point during login that should be emitted to the client.
// These will be RFC 2616-formatted errors with error code 299.
Warnings []string `json:"warnings"`
// Only used when ProviderType == "oidc". // Only used when ProviderType == "oidc".
OIDC *OIDCSessionData `json:"oidc,omitempty"` OIDC *OIDCSessionData `json:"oidc,omitempty"`

View File

@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
@ -726,8 +727,8 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUps
return u return u
} }
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAccessToken(token string) *TestUpstreamOIDCIdentityProviderBuilder { func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAccessToken(token string, expiry metav1.Time) *TestUpstreamOIDCIdentityProviderBuilder {
u.accessToken = &oidctypes.AccessToken{Token: token} u.accessToken = &oidctypes.AccessToken{Token: token, Expiry: expiry}
return u return u
} }

View File

@ -1,4 +1,4 @@
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. #! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -15,6 +15,8 @@ web:
https: 0.0.0.0:8443 https: 0.0.0.0:8443
tlsCert: /var/certs/dex.pem tlsCert: /var/certs/dex.pem
tlsKey: /var/certs/dex-key.pem tlsKey: /var/certs/dex-key.pem
expiry:
idTokens: 20m #! this is the lifetime for the id token as well as the access token.
oauth2: oauth2:
skipApprovalScreen: true skipApprovalScreen: true
#! Allow the resource owner password grant, which Dex implements to also return ID tokens. #! Allow the resource owner password grant, which Dex implements to also return ID tokens.