Merge remote-tracking branch 'origin/token-refresh' into token-exchange-endpoint
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
commit
a852baac75
@ -13,6 +13,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
@ -49,7 +50,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid
|
|||||||
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
|
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
|
||||||
cmd.Flags().StringVar(&clientID, "client-id", "", "OpenID Connect client ID.")
|
cmd.Flags().StringVar(&clientID, "client-id", "", "OpenID Connect client ID.")
|
||||||
cmd.Flags().Uint16Var(&listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only).")
|
cmd.Flags().Uint16Var(&listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only).")
|
||||||
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid"}, "OIDC scopes to request during login.")
|
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID}, "OIDC scopes to request during login.")
|
||||||
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
|
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
|
||||||
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
|
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
|
||||||
cmd.Flags().StringSliceVar(&caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated).")
|
cmd.Flags().StringSliceVar(&caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated).")
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
coreosoidc "github.com/coreos/go-oidc"
|
||||||
"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"
|
||||||
@ -57,7 +58,10 @@ func NewHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
||||||
grantOpenIDScopeIfRequested(authorizeRequester)
|
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||||
|
// There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope
|
||||||
|
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
|
||||||
|
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err = oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{
|
_, err = oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{
|
||||||
@ -148,14 +152,6 @@ func readCSRFCookie(r *http.Request, codec oidc.Codec) csrftoken.CSRFToken {
|
|||||||
return csrfFromCookie
|
return csrfFromCookie
|
||||||
}
|
}
|
||||||
|
|
||||||
func grantOpenIDScopeIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
|
||||||
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
|
||||||
if scope == "openid" {
|
|
||||||
authorizeRequester.GrantScope(scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func chooseUpstreamIDP(idpListGetter oidc.IDPListGetter) (provider.UpstreamOIDCIdentityProviderI, error) {
|
func chooseUpstreamIDP(idpListGetter oidc.IDPListGetter) (provider.UpstreamOIDCIdentityProviderI, error) {
|
||||||
allUpstreamIDPs := idpListGetter.GetIDPList()
|
allUpstreamIDPs := idpListGetter.GetIDPList()
|
||||||
if len(allUpstreamIDPs) == 0 {
|
if len(allUpstreamIDPs) == 0 {
|
||||||
|
@ -119,7 +119,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
Name: "some-idp",
|
Name: "some-idp",
|
||||||
ClientID: "some-client-id",
|
ClientID: "some-client-id",
|
||||||
AuthorizationURL: *upstreamAuthURL,
|
AuthorizationURL: *upstreamAuthURL,
|
||||||
Scopes: []string{"scope1", "scope2"},
|
Scopes: []string{"scope1", "scope2"}, // the scopes to request when starting the upstream authorization flow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
|
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
|
||||||
@ -372,6 +372,26 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "happy path when downstream requested scopes include offline_access",
|
||||||
|
issuer: downstreamIssuer,
|
||||||
|
idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider),
|
||||||
|
generateCSRF: happyCSRFGenerator,
|
||||||
|
generatePKCE: happyPKCEGenerator,
|
||||||
|
generateNonce: happyNonceGenerator,
|
||||||
|
stateEncoder: happyStateEncoder,
|
||||||
|
cookieEncoder: happyCookieEncoder,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid offline_access"}),
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: "text/html; charset=utf-8",
|
||||||
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
|
wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(map[string]string{
|
||||||
|
"scope": "openid offline_access",
|
||||||
|
}, "", "")),
|
||||||
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
|
wantBodyStringWithLocationInHref: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "downstream redirect uri does not match what is configured for client",
|
name: "downstream redirect uri does not match what is configured for client",
|
||||||
issuer: downstreamIssuer,
|
issuer: downstreamIssuer,
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
coreosoidc "github.com/coreos/go-oidc"
|
||||||
"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"
|
||||||
@ -70,8 +71,9 @@ func NewHandler(
|
|||||||
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grant the openid scope only if it was requested.
|
// Automatically grant the openid and offline_access scopes, but only if they were requested.
|
||||||
grantOpenIDScopeIfRequested(authorizeRequester)
|
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||||
|
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||||
|
|
||||||
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
||||||
r.Context(),
|
r.Context(),
|
||||||
@ -189,14 +191,6 @@ func readState(r *http.Request, stateDecoder oidc.Decoder) (*oidc.UpstreamStateP
|
|||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func grantOpenIDScopeIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
|
||||||
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
|
||||||
if scope == "openid" {
|
|
||||||
authorizeRequester.GrantScope(scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUsernameFromUpstreamIDToken(
|
func getUsernameFromUpstreamIDToken(
|
||||||
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
upstreamIDPConfig provider.UpstreamOIDCIdentityProviderI,
|
||||||
idTokenClaims map[string]interface{},
|
idTokenClaims map[string]interface{},
|
||||||
|
@ -66,7 +66,8 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
upstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"}
|
upstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"}
|
||||||
happyDownstreamScopesRequested = []string{"openid", "profile", "email"}
|
happyDownstreamScopesRequested = []string{"openid"}
|
||||||
|
happyDownstreamScopesGranted = []string{"openid"}
|
||||||
|
|
||||||
happyDownstreamRequestParamsQuery = url.Values{
|
happyDownstreamRequestParamsQuery = url.Values{
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
@ -127,7 +128,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantStatus int
|
wantStatus int
|
||||||
wantBody string
|
wantBody string
|
||||||
wantRedirectLocationRegexp string
|
wantRedirectLocationRegexp string
|
||||||
wantGrantedOpenidScope bool
|
wantDownstreamGrantedScopes []string
|
||||||
wantDownstreamIDTokenSubject string
|
wantDownstreamIDTokenSubject string
|
||||||
wantDownstreamIDTokenGroups []string
|
wantDownstreamIDTokenGroups []string
|
||||||
wantDownstreamRequestedScopes []string
|
wantDownstreamRequestedScopes []string
|
||||||
@ -145,11 +146,11 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantGrantedOpenidScope: true,
|
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamUsername,
|
wantDownstreamIDTokenSubject: upstreamUsername,
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
@ -163,11 +164,11 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantGrantedOpenidScope: true,
|
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + upstreamSubject,
|
||||||
wantDownstreamIDTokenGroups: nil,
|
wantDownstreamIDTokenGroups: nil,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
@ -181,11 +182,11 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantGrantedOpenidScope: true,
|
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: upstreamSubject,
|
wantDownstreamIDTokenSubject: upstreamSubject,
|
||||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
wantDownstreamNonce: downstreamNonce,
|
wantDownstreamNonce: downstreamNonce,
|
||||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
@ -316,6 +317,28 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "state's downstream auth params also included offline_access scope",
|
||||||
|
idp: happyUpstream().Build(),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().
|
||||||
|
WithState(
|
||||||
|
happyUpstreamStateParam().
|
||||||
|
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access"}).Encode()).
|
||||||
|
Build(t, happyStateCodec),
|
||||||
|
).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid%20offline_access&state=` + happyDownstreamState,
|
||||||
|
wantDownstreamIDTokenSubject: upstreamUsername,
|
||||||
|
wantDownstreamRequestedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantDownstreamGrantedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "the UpstreamOIDCProvider CRD has been deleted",
|
name: "the UpstreamOIDCProvider CRD has been deleted",
|
||||||
idp: otherUpstreamOIDCIdentityProvider,
|
idp: otherUpstreamOIDCIdentityProvider,
|
||||||
@ -481,7 +504,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
// Several Secrets should have been created
|
// Several Secrets should have been created
|
||||||
expectedNumberOfCreatedSecrets := 2
|
expectedNumberOfCreatedSecrets := 2
|
||||||
if test.wantGrantedOpenidScope {
|
if includesOpenIDScope(test.wantDownstreamGrantedScopes) {
|
||||||
expectedNumberOfCreatedSecrets++
|
expectedNumberOfCreatedSecrets++
|
||||||
}
|
}
|
||||||
require.Len(t, client.Actions(), expectedNumberOfCreatedSecrets)
|
require.Len(t, client.Actions(), expectedNumberOfCreatedSecrets)
|
||||||
@ -493,7 +516,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
oauthStore,
|
oauthStore,
|
||||||
authcodeDataAndSignature[1], // Authcode store key is authcode signature
|
authcodeDataAndSignature[1], // Authcode store key is authcode signature
|
||||||
test.wantGrantedOpenidScope,
|
test.wantDownstreamGrantedScopes,
|
||||||
test.wantDownstreamIDTokenSubject,
|
test.wantDownstreamIDTokenSubject,
|
||||||
test.wantDownstreamIDTokenGroups,
|
test.wantDownstreamIDTokenGroups,
|
||||||
test.wantDownstreamRequestedScopes,
|
test.wantDownstreamRequestedScopes,
|
||||||
@ -513,7 +536,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// One IDSession should have been stored, if the downstream actually requested the "openid" scope
|
// One IDSession should have been stored, if the downstream actually requested the "openid" scope
|
||||||
if test.wantGrantedOpenidScope {
|
if includesOpenIDScope(test.wantDownstreamGrantedScopes) {
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
||||||
|
|
||||||
validateIDSessionStorage(
|
validateIDSessionStorage(
|
||||||
@ -530,6 +553,15 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func includesOpenIDScope(scopes []string) bool {
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == "openid" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type requestPath struct {
|
type requestPath struct {
|
||||||
code, state *string
|
code, state *string
|
||||||
}
|
}
|
||||||
@ -704,7 +736,7 @@ func validateAuthcodeStorage(
|
|||||||
t *testing.T,
|
t *testing.T,
|
||||||
oauthStore *oidc.KubeStorage,
|
oauthStore *oidc.KubeStorage,
|
||||||
storeKey string,
|
storeKey string,
|
||||||
wantGrantedOpenidScope bool,
|
wantDownstreamGrantedScopes []string,
|
||||||
wantDownstreamIDTokenSubject string,
|
wantDownstreamIDTokenSubject string,
|
||||||
wantDownstreamIDTokenGroups []string,
|
wantDownstreamIDTokenGroups []string,
|
||||||
wantDownstreamRequestedScopes []string,
|
wantDownstreamRequestedScopes []string,
|
||||||
@ -719,11 +751,7 @@ func validateAuthcodeStorage(
|
|||||||
storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode)
|
storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode)
|
||||||
|
|
||||||
// Check which scopes were granted.
|
// Check which scopes were granted.
|
||||||
if wantGrantedOpenidScope {
|
require.ElementsMatch(t, wantDownstreamGrantedScopes, storedRequestFromAuthcode.GetGrantedScopes())
|
||||||
require.Contains(t, storedRequestFromAuthcode.GetGrantedScopes(), "openid")
|
|
||||||
} else {
|
|
||||||
require.NotContains(t, storedRequestFromAuthcode.GetGrantedScopes(), "openid")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all the other fields of the stored request.
|
// Check all the other fields of the stored request.
|
||||||
require.NotEmpty(t, storedRequestFromAuthcode.ID)
|
require.NotEmpty(t, storedRequestFromAuthcode.ID)
|
||||||
|
@ -41,73 +41,147 @@ func NewKubeStorage(secrets corev1client.SecretInterface) *KubeStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
//
|
||||||
return k.refreshTokenStorage.RevokeRefreshToken(ctx, requestID)
|
// Authorization Code sessions:
|
||||||
|
//
|
||||||
|
// These are keyed by the signature of the authcode.
|
||||||
|
//
|
||||||
|
// Fosite will create these in the authorize endpoint.
|
||||||
|
//
|
||||||
|
// Fosite will never delete them. Instead, it wants to mark them as invalidated once the authcode is used to redeem tokens.
|
||||||
|
// That way, it can later detect the case where an authcode that was already redeemed gets used again.
|
||||||
|
//
|
||||||
|
|
||||||
|
func (k KubeStorage) CreateAuthorizeCodeSession(ctx context.Context, signatureOfAuthcode string, r fosite.Requester) (err error) {
|
||||||
|
return k.authorizationCodeStorage.CreateAuthorizeCodeSession(ctx, signatureOfAuthcode, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) GetAuthorizeCodeSession(ctx context.Context, signatureOfAuthcode string, s fosite.Session) (request fosite.Requester, err error) {
|
||||||
|
return k.authorizationCodeStorage.GetAuthorizeCodeSession(ctx, signatureOfAuthcode, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) InvalidateAuthorizeCodeSession(ctx context.Context, signatureOfAuthcode string) (err error) {
|
||||||
|
return k.authorizationCodeStorage.InvalidateAuthorizeCodeSession(ctx, signatureOfAuthcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// PKCE sessions:
|
||||||
|
//
|
||||||
|
// These are keyed by the signature of the authcode.
|
||||||
|
//
|
||||||
|
// Fosite will create these in the authorize endpoint at the same time that it is creating an authcode.
|
||||||
|
//
|
||||||
|
// Fosite will delete these in the token endpoint during authcode redemption since they are no longer needed after that.
|
||||||
|
// If the user chooses to never redeem their authcode, then fosite will never delete these.
|
||||||
|
//
|
||||||
|
|
||||||
|
func (k KubeStorage) CreatePKCERequestSession(ctx context.Context, signatureOfAuthcode string, requester fosite.Requester) error {
|
||||||
|
return k.pkceStorage.CreatePKCERequestSession(ctx, signatureOfAuthcode, requester)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) GetPKCERequestSession(ctx context.Context, signatureOfAuthcode string, session fosite.Session) (fosite.Requester, error) {
|
||||||
|
return k.pkceStorage.GetPKCERequestSession(ctx, signatureOfAuthcode, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) DeletePKCERequestSession(ctx context.Context, signatureOfAuthcode string) error {
|
||||||
|
return k.pkceStorage.DeletePKCERequestSession(ctx, signatureOfAuthcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// OpenID Connect sessions:
|
||||||
|
//
|
||||||
|
// These are keyed by the full value of the authcode (not just the signature).
|
||||||
|
//
|
||||||
|
// Fosite will create these in the authorize endpoint when it creates an authcode, but only if the user
|
||||||
|
// requested the openid scope.
|
||||||
|
//
|
||||||
|
// Fosite will never delete these, which is likely a bug in fosite. Although there is a delete method below, fosite
|
||||||
|
// never calls it. Used during authcode redemption, they will never be accessed again after a successful authcode
|
||||||
|
// redemption. Although that implies that they should probably follow a lifecycle similar the the PKCE storage, they
|
||||||
|
// are, in fact, not deleted.
|
||||||
|
//
|
||||||
|
|
||||||
|
func (k KubeStorage) CreateOpenIDConnectSession(ctx context.Context, fullAuthcode string, requester fosite.Requester) error {
|
||||||
|
return k.oidcStorage.CreateOpenIDConnectSession(ctx, fullAuthcode, requester)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) GetOpenIDConnectSession(ctx context.Context, fullAuthcode string, requester fosite.Requester) (fosite.Requester, error) {
|
||||||
|
return k.oidcStorage.GetOpenIDConnectSession(ctx, fullAuthcode, requester)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) DeleteOpenIDConnectSession(ctx context.Context, fullAuthcode string) error {
|
||||||
|
return k.oidcStorage.DeleteOpenIDConnectSession(ctx, fullAuthcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Access token sessions:
|
||||||
|
//
|
||||||
|
// These are keyed by the signature of the access token.
|
||||||
|
//
|
||||||
|
// Fosite will create these in the token endpoint whenever it wants to hand out an access token, including the original
|
||||||
|
// authcode redemption and also during refresh.
|
||||||
|
//
|
||||||
|
// Fosite will not use the delete method. Instead, it will use the revoke method to delete them.
|
||||||
|
// During a refresh in the token endpoint, the old access token is revoked just before the new access token is created.
|
||||||
|
// Also, if the token endpoint receives an authcode that was already used successfully, then it revokes the access token
|
||||||
|
// that was previously handed out for that authcode. If a user stops coming back to refresh their tokens, then that
|
||||||
|
// access token will never be deleted.
|
||||||
|
//
|
||||||
|
|
||||||
|
func (k KubeStorage) CreateAccessTokenSession(ctx context.Context, signatureOfAccessToken string, requester fosite.Requester) (err error) {
|
||||||
|
return k.accessTokenStorage.CreateAccessTokenSession(ctx, signatureOfAccessToken, requester)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) GetAccessTokenSession(ctx context.Context, signatureOfAccessToken string, session fosite.Session) (request fosite.Requester, err error) {
|
||||||
|
return k.accessTokenStorage.GetAccessTokenSession(ctx, signatureOfAccessToken, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KubeStorage) DeleteAccessTokenSession(ctx context.Context, signatureOfAccessToken string) (err error) {
|
||||||
|
return k.accessTokenStorage.DeleteAccessTokenSession(ctx, signatureOfAccessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) RevokeAccessToken(ctx context.Context, requestID string) error {
|
func (k KubeStorage) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||||
return k.accessTokenStorage.RevokeAccessToken(ctx, requestID)
|
return k.accessTokenStorage.RevokeAccessToken(ctx, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
//
|
||||||
return k.refreshTokenStorage.CreateRefreshTokenSession(ctx, signature, request)
|
// Refresh token sessions:
|
||||||
|
//
|
||||||
|
// These are keyed by the signature of the refresh token.
|
||||||
|
//
|
||||||
|
// Fosite will create these in the token endpoint whenever it wants to hand out an refresh token, including the original
|
||||||
|
// authcode redemption and also during refresh. Refresh tokens are only handed out when the user requested the
|
||||||
|
// offline_access scope on the original authorization request.
|
||||||
|
//
|
||||||
|
// Fosite will not use the delete method. Instead, it will use the revoke method to delete them.
|
||||||
|
// During a refresh in the token endpoint, the old refresh token is revoked just before the new refresh token is created.
|
||||||
|
// Also, if the token endpoint receives an authcode that was already used successfully, then it revokes the refresh token
|
||||||
|
// that was previously handed out for that authcode. If a user stops coming back to refresh their tokens, then that
|
||||||
|
// refresh token will never be deleted.
|
||||||
|
//
|
||||||
|
|
||||||
|
func (k KubeStorage) CreateRefreshTokenSession(ctx context.Context, signatureOfRefreshToken string, request fosite.Requester) (err error) {
|
||||||
|
return k.refreshTokenStorage.CreateRefreshTokenSession(ctx, signatureOfRefreshToken, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
func (k KubeStorage) GetRefreshTokenSession(ctx context.Context, signatureOfRefreshToken string, session fosite.Session) (request fosite.Requester, err error) {
|
||||||
return k.refreshTokenStorage.GetRefreshTokenSession(ctx, signature, session)
|
return k.refreshTokenStorage.GetRefreshTokenSession(ctx, signatureOfRefreshToken, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) {
|
func (k KubeStorage) DeleteRefreshTokenSession(ctx context.Context, signatureOfRefreshToken string) (err error) {
|
||||||
return k.refreshTokenStorage.DeleteRefreshTokenSession(ctx, signature)
|
return k.refreshTokenStorage.DeleteRefreshTokenSession(ctx, signatureOfRefreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) CreateAccessTokenSession(ctx context.Context, signature string, requester fosite.Requester) (err error) {
|
func (k KubeStorage) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||||
return k.accessTokenStorage.CreateAccessTokenSession(ctx, signature, requester)
|
return k.refreshTokenStorage.RevokeRefreshToken(ctx, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
//
|
||||||
return k.accessTokenStorage.GetAccessTokenSession(ctx, signature, session)
|
// OAuth client definitions:
|
||||||
}
|
//
|
||||||
|
// For the time being, we only allow a single pre-defined client, so we do not need to interact with any underlying
|
||||||
func (k KubeStorage) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) {
|
// storage mechanism to fetch them.
|
||||||
return k.accessTokenStorage.DeleteAccessTokenSession(ctx, signature)
|
//
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) CreateOpenIDConnectSession(ctx context.Context, authcode string, requester fosite.Requester) error {
|
|
||||||
return k.oidcStorage.CreateOpenIDConnectSession(ctx, authcode, requester)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) GetOpenIDConnectSession(ctx context.Context, authcode string, requester fosite.Requester) (fosite.Requester, error) {
|
|
||||||
return k.oidcStorage.GetOpenIDConnectSession(ctx, authcode, requester)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) DeleteOpenIDConnectSession(ctx context.Context, authcode string) error {
|
|
||||||
return k.oidcStorage.DeleteOpenIDConnectSession(ctx, authcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
|
|
||||||
return k.pkceStorage.GetPKCERequestSession(ctx, signature, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) CreatePKCERequestSession(ctx context.Context, signature string, requester fosite.Requester) error {
|
|
||||||
return k.pkceStorage.CreatePKCERequestSession(ctx, signature, requester)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) DeletePKCERequestSession(ctx context.Context, signature string) error {
|
|
||||||
return k.pkceStorage.DeletePKCERequestSession(ctx, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) CreateAuthorizeCodeSession(ctx context.Context, signature string, r fosite.Requester) (err error) {
|
|
||||||
return k.authorizationCodeStorage.CreateAuthorizeCodeSession(ctx, signature, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) GetAuthorizeCodeSession(ctx context.Context, signature string, s fosite.Session) (request fosite.Requester, err error) {
|
|
||||||
return k.authorizationCodeStorage.GetAuthorizeCodeSession(ctx, signature, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KubeStorage) InvalidateAuthorizeCodeSession(ctx context.Context, signature string) (err error) {
|
|
||||||
return k.authorizationCodeStorage.InvalidateAuthorizeCodeSession(ctx, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (KubeStorage) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
func (KubeStorage) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||||
client := PinnipedCLIOIDCClient()
|
client := PinnipedCLIOIDCClient()
|
||||||
@ -117,6 +191,10 @@ func (KubeStorage) GetClient(_ context.Context, id string) (fosite.Client, error
|
|||||||
return nil, fosite.ErrNotFound
|
return nil, fosite.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Unused interface methods.
|
||||||
|
//
|
||||||
|
|
||||||
func (KubeStorage) ClientAssertionJWTValid(_ context.Context, _ string) error {
|
func (KubeStorage) ClientAssertionJWTValid(_ context.Context, _ string) error {
|
||||||
return errKubeStorageNotImplemented
|
return errKubeStorageNotImplemented
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ func TestNullStorage_GetClient(t *testing.T) {
|
|||||||
Public: true,
|
Public: true,
|
||||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||||
ResponseTypes: []string{"code"},
|
ResponseTypes: []string{"code"},
|
||||||
GrantTypes: []string{"authorization_code"},
|
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||||
Scopes: []string{"openid", "profile", "email"},
|
Scopes: []string{"openid", "offline_access", "profile", "email"},
|
||||||
},
|
},
|
||||||
TokenEndpointAuthMethod: "none",
|
TokenEndpointAuthMethod: "none",
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,7 @@ package oidc
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
coreosoidc "github.com/coreos/go-oidc"
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
|
|
||||||
@ -84,7 +85,7 @@ func PinnipedCLIOIDCClient() *fosite.DefaultOpenIDConnectClient {
|
|||||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||||
ResponseTypes: []string{"code"},
|
ResponseTypes: []string{"code"},
|
||||||
GrantTypes: []string{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
GrantTypes: []string{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||||
Scopes: []string{"openid", "profile", "email"},
|
Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email"},
|
||||||
},
|
},
|
||||||
TokenEndpointAuthMethod: "none",
|
TokenEndpointAuthMethod: "none",
|
||||||
}
|
}
|
||||||
@ -111,7 +112,8 @@ func FositeOauth2Helper(
|
|||||||
EnforcePKCE: true, // follow current set of best practices and always require PKCE
|
EnforcePKCE: true, // follow current set of best practices and always require PKCE
|
||||||
AllowedPromptValues: []string{"none"}, // TODO unclear what we should set here
|
AllowedPromptValues: []string{"none"}, // TODO unclear what we should set here
|
||||||
|
|
||||||
RefreshTokenScopes: nil, // TODO decide what makes sense when we add refresh token support
|
RefreshTokenScopes: []string{coreosoidc.ScopeOfflineAccess}, // as per https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||||
|
|
||||||
MinParameterEntropy: 32, // 256 bits seems about right
|
MinParameterEntropy: 32, // 256 bits seems about right
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,3 +159,11 @@ func FositeErrorForLog(err error) []interface{} {
|
|||||||
type IDPListGetter interface {
|
type IDPListGetter interface {
|
||||||
GetIDPList() []provider.UpstreamOIDCIdentityProviderI
|
GetIDPList() []provider.UpstreamOIDCIdentityProviderI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) {
|
||||||
|
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
||||||
|
if scope == scopeName {
|
||||||
|
authorizeRequester.GrantScope(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -211,8 +211,9 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
||||||
authRequest func(authRequest *http.Request)
|
modifyAuthRequest func(authRequest *http.Request)
|
||||||
storage func(
|
modifyTokenRequest func(r *http.Request, authCode string)
|
||||||
|
modifyStorage func(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
s interface {
|
s interface {
|
||||||
oauth2.TokenRevocationStorage
|
oauth2.TokenRevocationStorage
|
||||||
@ -223,7 +224,6 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
authCode string,
|
authCode string,
|
||||||
)
|
)
|
||||||
request func(r *http.Request, authCode string)
|
|
||||||
makeOathHelper func(
|
makeOathHelper func(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
authRequest *http.Request,
|
authRequest *http.Request,
|
||||||
@ -239,56 +239,77 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
wantStatus int
|
wantStatus int
|
||||||
wantBodyFields []string
|
wantBodyFields []string
|
||||||
wantExactBody string
|
wantExactBody string
|
||||||
|
wantRequestedScopes []string
|
||||||
}{
|
}{
|
||||||
// happy path
|
// happy path
|
||||||
{
|
{
|
||||||
name: "request is valid and tokens are issued",
|
name: "request is valid and tokens are issued",
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"},
|
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
|
||||||
|
wantRequestedScopes: []string{"openid", "profile", "email"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "openid scope was not requested from authorize endpoint",
|
name: "openid scope was not requested from authorize endpoint",
|
||||||
authRequest: func(authRequest *http.Request) {
|
modifyAuthRequest: func(authRequest *http.Request) {
|
||||||
authRequest.Form.Set("scope", "profile email")
|
authRequest.Form.Set("scope", "profile email")
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in"},
|
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in"}, // no id or refresh tokens
|
||||||
|
wantRequestedScopes: []string{"profile", "email"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "offline_access and openid scopes were requested and granted from authorize endpoint",
|
||||||
|
modifyAuthRequest: func(authRequest *http.Request) {
|
||||||
|
authRequest.Form.Set("scope", "openid offline_access")
|
||||||
|
},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens
|
||||||
|
wantRequestedScopes: []string{"openid", "offline_access"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "offline_access (without openid scope) was requested and granted from authorize endpoint",
|
||||||
|
modifyAuthRequest: func(authRequest *http.Request) {
|
||||||
|
authRequest.Form.Set("scope", "offline_access")
|
||||||
|
},
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in", "refresh_token"}, // no id token
|
||||||
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// sad path
|
// sad path
|
||||||
{
|
{
|
||||||
name: "GET method is wrong",
|
name: "GET method is wrong",
|
||||||
request: func(r *http.Request, authCode string) { r.Method = http.MethodGet },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodGet },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeInvalidMethodErrorBody("GET"),
|
wantExactBody: fositeInvalidMethodErrorBody("GET"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PUT method is wrong",
|
name: "PUT method is wrong",
|
||||||
request: func(r *http.Request, authCode string) { r.Method = http.MethodPut },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPut },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeInvalidMethodErrorBody("PUT"),
|
wantExactBody: fositeInvalidMethodErrorBody("PUT"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PATCH method is wrong",
|
name: "PATCH method is wrong",
|
||||||
request: func(r *http.Request, authCode string) { r.Method = http.MethodPatch },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPatch },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeInvalidMethodErrorBody("PATCH"),
|
wantExactBody: fositeInvalidMethodErrorBody("PATCH"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DELETE method is wrong",
|
name: "DELETE method is wrong",
|
||||||
request: func(r *http.Request, authCode string) { r.Method = http.MethodDelete },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodDelete },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeInvalidMethodErrorBody("DELETE"),
|
wantExactBody: fositeInvalidMethodErrorBody("DELETE"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "content type is invalid",
|
name: "content type is invalid",
|
||||||
request: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeEmptyPayloadErrorBody,
|
wantExactBody: fositeEmptyPayloadErrorBody,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payload is not valid form serialization",
|
name: "payload is not valid form serialization",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = ioutil.NopCloser(strings.NewReader("this newline character is not allowed in a form serialization: \n"))
|
r.Body = ioutil.NopCloser(strings.NewReader("this newline character is not allowed in a form serialization: \n"))
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -296,13 +317,13 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payload is empty",
|
name: "payload is empty",
|
||||||
request: func(r *http.Request, authCode string) { r.Body = nil },
|
modifyTokenRequest: func(r *http.Request, authCode string) { r.Body = nil },
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
wantExactBody: fositeInvalidPayloadErrorBody,
|
wantExactBody: fositeInvalidPayloadErrorBody,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "grant type is missing in request",
|
name: "grant type is missing in request",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithGrantType("").ReadCloser()
|
r.Body = happyBody(authCode).WithGrantType("").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -310,7 +331,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "grant type is not authorization_code",
|
name: "grant type is not authorization_code",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithGrantType("bogus").ReadCloser()
|
r.Body = happyBody(authCode).WithGrantType("bogus").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -318,7 +339,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client id is missing in request",
|
name: "client id is missing in request",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithClientID("").ReadCloser()
|
r.Body = happyBody(authCode).WithClientID("").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -326,7 +347,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client id is wrong",
|
name: "client id is wrong",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithClientID("bogus").ReadCloser()
|
r.Body = happyBody(authCode).WithClientID("bogus").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusUnauthorized,
|
wantStatus: http.StatusUnauthorized,
|
||||||
@ -334,7 +355,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "auth code is missing in request",
|
name: "auth code is missing in request",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithAuthCode("").ReadCloser()
|
r.Body = happyBody(authCode).WithAuthCode("").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -342,7 +363,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "auth code has never been valid",
|
name: "auth code has never been valid",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithAuthCode("bogus").ReadCloser()
|
r.Body = happyBody(authCode).WithAuthCode("bogus").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -350,7 +371,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "auth code is invalidated",
|
name: "auth code is invalidated",
|
||||||
storage: func(
|
modifyStorage: func(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
s interface {
|
s interface {
|
||||||
oauth2.TokenRevocationStorage
|
oauth2.TokenRevocationStorage
|
||||||
@ -369,7 +390,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "redirect uri is missing in request",
|
name: "redirect uri is missing in request",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithRedirectURI("").ReadCloser()
|
r.Body = happyBody(authCode).WithRedirectURI("").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -377,7 +398,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "redirect uri is wrong",
|
name: "redirect uri is wrong",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithRedirectURI("bogus").ReadCloser()
|
r.Body = happyBody(authCode).WithRedirectURI("bogus").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -385,7 +406,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pkce is missing in request",
|
name: "pkce is missing in request",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithPKCE("").ReadCloser()
|
r.Body = happyBody(authCode).WithPKCE("").ReadCloser()
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusBadRequest,
|
wantStatus: http.StatusBadRequest,
|
||||||
@ -393,7 +414,7 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pkce is wrong",
|
name: "pkce is wrong",
|
||||||
request: func(r *http.Request, authCode string) {
|
modifyTokenRequest: func(r *http.Request, authCode string) {
|
||||||
r.Body = happyBody(authCode).WithPKCE(
|
r.Body = happyBody(authCode).WithPKCE(
|
||||||
"bogus-verifier-that-is-at-least-43-characters-for-the-sake-of-entropy",
|
"bogus-verifier-that-is-at-least-43-characters-for-the-sake-of-entropy",
|
||||||
).ReadCloser()
|
).ReadCloser()
|
||||||
@ -412,8 +433,8 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
test := test
|
test := test
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
authRequest := deepCopyRequestForm(happyAuthRequest)
|
authRequest := deepCopyRequestForm(happyAuthRequest)
|
||||||
if test.authRequest != nil {
|
if test.modifyAuthRequest != nil {
|
||||||
test.authRequest(authRequest)
|
test.modifyAuthRequest(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
@ -430,24 +451,26 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore)
|
oauthHelper, authCode, jwtSigningKey = makeHappyOauthHelper(t, authRequest, oauthStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.storage != nil {
|
if test.modifyStorage != nil {
|
||||||
test.storage(t, oauthStore, authCode)
|
test.modifyStorage(t, oauthStore, authCode)
|
||||||
}
|
}
|
||||||
subject := NewHandler(oauthHelper)
|
subject := NewHandler(oauthHelper)
|
||||||
|
|
||||||
|
authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid")
|
||||||
|
expectedNumberOfIDSessionsStored := 0
|
||||||
|
if authorizeEndpointGrantedOpenIDScope {
|
||||||
|
expectedNumberOfIDSessionsStored = 1
|
||||||
|
}
|
||||||
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 1)
|
||||||
if strings.Contains(authRequest.Form.Get("scope"), "openid") {
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, expectedNumberOfIDSessionsStored)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2+expectedNumberOfIDSessionsStored)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 3)
|
|
||||||
} else {
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
if test.request != nil {
|
if test.modifyTokenRequest != nil {
|
||||||
test.request(req, authCode)
|
test.modifyTokenRequest(req, authCode)
|
||||||
}
|
}
|
||||||
rsp := httptest.NewRecorder()
|
rsp := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -458,29 +481,38 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
require.Equal(t, test.wantStatus, rsp.Code)
|
require.Equal(t, test.wantStatus, rsp.Code)
|
||||||
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), "application/json")
|
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), "application/json")
|
||||||
if test.wantBodyFields != nil {
|
if test.wantBodyFields != nil {
|
||||||
var m map[string]interface{}
|
var parsedResponseBody map[string]interface{}
|
||||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &m))
|
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
|
||||||
require.ElementsMatch(t, test.wantBodyFields, getMapKeys(m))
|
require.ElementsMatch(t, test.wantBodyFields, getMapKeys(parsedResponseBody))
|
||||||
|
|
||||||
|
wantIDToken := contains(test.wantBodyFields, "id_token")
|
||||||
|
wantRefreshToken := contains(test.wantBodyFields, "refresh_token")
|
||||||
|
|
||||||
code := req.PostForm.Get("code")
|
code := req.PostForm.Get("code")
|
||||||
wantOpenidScope := contains(test.wantBodyFields, "id_token")
|
|
||||||
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
||||||
requireValidAccessTokenStorage(t, m, oauthStore, wantOpenidScope)
|
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
|
||||||
requireInvalidPKCEStorage(t, code, oauthStore)
|
requireInvalidPKCEStorage(t, code, oauthStore)
|
||||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
|
||||||
|
|
||||||
|
expectedNumberOfRefreshTokenSessionsStored := 0
|
||||||
|
if wantRefreshToken {
|
||||||
|
expectedNumberOfRefreshTokenSessionsStored = 1
|
||||||
|
}
|
||||||
|
expectedNumberOfIDSessionsStored = 0
|
||||||
|
if wantIDToken {
|
||||||
|
expectedNumberOfIDSessionsStored = 1
|
||||||
|
requireValidIDToken(t, parsedResponseBody, jwtSigningKey)
|
||||||
|
}
|
||||||
|
if wantRefreshToken {
|
||||||
|
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
||||||
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, expectedNumberOfRefreshTokenSessionsStored)
|
||||||
if wantOpenidScope {
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, expectedNumberOfIDSessionsStored)
|
||||||
requireValidIDToken(t, m, jwtSigningKey)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2+expectedNumberOfRefreshTokenSessionsStored+expectedNumberOfIDSessionsStored)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 3)
|
|
||||||
} else {
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
require.JSONEq(t, test.wantExactBody, rsp.Body.String())
|
require.JSONEq(t, test.wantExactBody, rsp.Body.String())
|
||||||
}
|
}
|
||||||
@ -489,6 +521,12 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("auth code is used twice", func(t *testing.T) {
|
t.Run("auth code is used twice", func(t *testing.T) {
|
||||||
authRequest := deepCopyRequestForm(happyAuthRequest)
|
authRequest := deepCopyRequestForm(happyAuthRequest)
|
||||||
|
authRequest.Form.Set("scope", "openid offline_access profile email")
|
||||||
|
|
||||||
|
wantRequestedScopes := []string{"openid", "offline_access", "profile", "email"}
|
||||||
|
wantBodyFields := []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}
|
||||||
|
wantGrantedOpenidScope := true
|
||||||
|
wantGrantedOfflineAccessScope := true
|
||||||
|
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets("some-namespace")
|
secrets := client.CoreV1().Secrets("some-namespace")
|
||||||
@ -513,26 +551,25 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
testutil.RequireEqualContentType(t, rsp0.Header().Get("Content-Type"), "application/json")
|
testutil.RequireEqualContentType(t, rsp0.Header().Get("Content-Type"), "application/json")
|
||||||
require.Equal(t, http.StatusOK, rsp0.Code)
|
require.Equal(t, http.StatusOK, rsp0.Code)
|
||||||
|
|
||||||
var m map[string]interface{}
|
var parsedResponseBody map[string]interface{}
|
||||||
require.NoError(t, json.Unmarshal(rsp0.Body.Bytes(), &m))
|
require.NoError(t, json.Unmarshal(rsp0.Body.Bytes(), &parsedResponseBody))
|
||||||
|
|
||||||
wantBodyFields := []string{"id_token", "access_token", "token_type", "expires_in", "scope"}
|
require.ElementsMatch(t, wantBodyFields, getMapKeys(parsedResponseBody))
|
||||||
require.ElementsMatch(t, wantBodyFields, getMapKeys(m))
|
|
||||||
|
requireValidIDToken(t, parsedResponseBody, jwtSigningKey)
|
||||||
|
|
||||||
code := req.PostForm.Get("code")
|
code := req.PostForm.Get("code")
|
||||||
wantOpenidScope := true
|
|
||||||
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
||||||
requireValidAccessTokenStorage(t, m, oauthStore, wantOpenidScope)
|
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
|
||||||
requireInvalidPKCEStorage(t, code, oauthStore)
|
requireInvalidPKCEStorage(t, code, oauthStore)
|
||||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
|
||||||
requireValidIDToken(t, m, jwtSigningKey)
|
|
||||||
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 1)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 3)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 1)
|
||||||
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 1)
|
||||||
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
||||||
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 4)
|
||||||
|
|
||||||
// Second call - should be unsuccessful since auth code was already used.
|
// Second call - should be unsuccessful since auth code was already used.
|
||||||
//
|
//
|
||||||
@ -546,16 +583,21 @@ func TestTokenEndpoint(t *testing.T) {
|
|||||||
testutil.RequireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json")
|
testutil.RequireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json")
|
||||||
require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String())
|
require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String())
|
||||||
|
|
||||||
|
// This was previously invalidated by the first request, so it remains invalidated
|
||||||
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
requireInvalidAuthCodeStorage(t, code, oauthStore)
|
||||||
requireInvalidAccessTokenStorage(t, m, oauthStore)
|
// Has now invalidated the access token that was previously handed out by the first request
|
||||||
|
requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore)
|
||||||
|
// This was previously invalidated by the first request, so it remains invalidated
|
||||||
requireInvalidPKCEStorage(t, code, oauthStore)
|
requireInvalidPKCEStorage(t, code, oauthStore)
|
||||||
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
|
// Fosite never cleans up OpenID Connect session storage, so it is still there
|
||||||
|
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
|
||||||
|
|
||||||
|
// 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)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
|
||||||
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 0)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 0)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
|
|
||||||
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
|
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -671,6 +713,9 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques
|
|||||||
if strings.Contains(authRequest.Form.Get("scope"), "openid") {
|
if strings.Contains(authRequest.Form.Get("scope"), "openid") {
|
||||||
authRequester.GrantScope("openid")
|
authRequester.GrantScope("openid")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(authRequest.Form.Get("scope"), "offline_access") {
|
||||||
|
authRequester.GrantScope("offline_access")
|
||||||
|
}
|
||||||
authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session)
|
authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return authResponder
|
return authResponder
|
||||||
@ -705,11 +750,44 @@ func requireInvalidAuthCodeStorage(
|
|||||||
require.True(t, errors.Is(err, fosite.ErrInvalidatedAuthorizeCode))
|
require.True(t, errors.Is(err, fosite.ErrInvalidatedAuthorizeCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireValidRefreshTokenStorage(
|
||||||
|
t *testing.T,
|
||||||
|
body map[string]interface{},
|
||||||
|
storage oauth2.CoreStorage,
|
||||||
|
wantRequestedScopes []string,
|
||||||
|
wantGrantedOpenidScope bool,
|
||||||
|
wantGrantedOfflineAccessScope bool,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Get the refresh token, and make sure we can use it to perform a lookup on the storage.
|
||||||
|
refreshToken, ok := body["refresh_token"]
|
||||||
|
require.True(t, ok)
|
||||||
|
refreshTokenString, ok := refreshToken.(string)
|
||||||
|
require.Truef(t, ok, "wanted refresh_token to be a string, but got %T", refreshToken)
|
||||||
|
require.NotEmpty(t, refreshTokenString)
|
||||||
|
storedRequest, err := storage.GetRefreshTokenSession(context.Background(), getFositeDataSignature(t, refreshTokenString), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Fosite stores refresh tokens without any of the original request form parameters.
|
||||||
|
requireValidStoredRequest(
|
||||||
|
t,
|
||||||
|
storedRequest,
|
||||||
|
storedRequest.Sanitize([]string{}).GetRequestForm(),
|
||||||
|
wantRequestedScopes,
|
||||||
|
wantGrantedOpenidScope,
|
||||||
|
wantGrantedOfflineAccessScope,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func requireValidAccessTokenStorage(
|
func requireValidAccessTokenStorage(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
body map[string]interface{},
|
body map[string]interface{},
|
||||||
storage oauth2.CoreStorage,
|
storage oauth2.CoreStorage,
|
||||||
|
wantRequestedScopes []string,
|
||||||
wantGrantedOpenidScope bool,
|
wantGrantedOpenidScope bool,
|
||||||
|
wantGrantedOfflineAccessScope bool,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -718,7 +796,8 @@ func requireValidAccessTokenStorage(
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
accessTokenString, ok := accessToken.(string)
|
accessTokenString, ok := accessToken.(string)
|
||||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||||
authRequest, err := storage.GetAccessTokenSession(context.Background(), getFositeDataSignature(t, accessTokenString), nil)
|
require.NotEmpty(t, accessTokenString)
|
||||||
|
storedRequest, err := storage.GetAccessTokenSession(context.Background(), getFositeDataSignature(t, accessTokenString), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Make sure the other body fields are valid.
|
// Make sure the other body fields are valid.
|
||||||
@ -736,24 +815,33 @@ func requireValidAccessTokenStorage(
|
|||||||
|
|
||||||
scopes, ok := body["scope"]
|
scopes, ok := body["scope"]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
scopesString, ok := scopes.(string)
|
actualGrantedScopesString, ok := scopes.(string)
|
||||||
require.Truef(t, ok, "wanted scopes to be an string, but got %T", scopes)
|
require.Truef(t, ok, "wanted scopes to be an string, but got %T", scopes)
|
||||||
wantScopes := ""
|
require.Equal(t, strings.Join(wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope), " "), actualGrantedScopesString)
|
||||||
if wantGrantedOpenidScope {
|
|
||||||
wantScopes += "openid"
|
|
||||||
}
|
|
||||||
require.Equal(t, wantScopes, scopesString)
|
|
||||||
|
|
||||||
// Fosite stores access tokens without any of the original request form pararmeters.
|
// Fosite stores access tokens without any of the original request form parameters.
|
||||||
requireValidAuthRequest(
|
requireValidStoredRequest(
|
||||||
t,
|
t,
|
||||||
authRequest,
|
storedRequest,
|
||||||
authRequest.Sanitize([]string{}).GetRequestForm(),
|
storedRequest.Sanitize([]string{}).GetRequestForm(),
|
||||||
|
wantRequestedScopes,
|
||||||
wantGrantedOpenidScope,
|
wantGrantedOpenidScope,
|
||||||
|
wantGrantedOfflineAccessScope,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope bool) []string {
|
||||||
|
scopesWanted := []string{}
|
||||||
|
if wantGrantedOpenidScope {
|
||||||
|
scopesWanted = append(scopesWanted, "openid")
|
||||||
|
}
|
||||||
|
if wantGrantedOfflineAccessScope {
|
||||||
|
scopesWanted = append(scopesWanted, "offline_access")
|
||||||
|
}
|
||||||
|
return scopesWanted
|
||||||
|
}
|
||||||
|
|
||||||
func requireInvalidAccessTokenStorage(
|
func requireInvalidAccessTokenStorage(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
body map[string]interface{},
|
body map[string]interface{},
|
||||||
@ -788,13 +876,15 @@ func requireValidOIDCStorage(
|
|||||||
body map[string]interface{},
|
body map[string]interface{},
|
||||||
code string,
|
code string,
|
||||||
storage openid.OpenIDConnectRequestStorage,
|
storage openid.OpenIDConnectRequestStorage,
|
||||||
|
wantRequestedScopes []string,
|
||||||
wantGrantedOpenidScope bool,
|
wantGrantedOpenidScope bool,
|
||||||
|
wantGrantedOfflineAccessScope bool,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if wantGrantedOpenidScope {
|
if wantGrantedOpenidScope {
|
||||||
// Make sure the OIDC session is still there. Note that Fosite stores OIDC sessions using the full auth code as a key.
|
// Make sure the OIDC session is still there. Note that Fosite stores OIDC sessions using the full auth code as a key.
|
||||||
authRequest, err := storage.GetOpenIDConnectSession(context.Background(), code, nil)
|
storedRequest, err := storage.GetOpenIDConnectSession(context.Background(), code, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Fosite stores OIDC sessions with only the nonce in the original request form.
|
// Fosite stores OIDC sessions with only the nonce in the original request form.
|
||||||
@ -804,11 +894,13 @@ func requireValidOIDCStorage(
|
|||||||
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
|
||||||
require.NotEmpty(t, accessTokenString)
|
require.NotEmpty(t, accessTokenString)
|
||||||
|
|
||||||
requireValidAuthRequest(
|
requireValidStoredRequest(
|
||||||
t,
|
t,
|
||||||
authRequest,
|
storedRequest,
|
||||||
authRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
|
storedRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
|
||||||
true,
|
wantRequestedScopes,
|
||||||
|
wantGrantedOpenidScope,
|
||||||
|
wantGrantedOfflineAccessScope,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -817,34 +909,30 @@ func requireValidOIDCStorage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requireValidAuthRequest(
|
func requireValidStoredRequest(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
authRequest fosite.Requester,
|
request fosite.Requester,
|
||||||
wantRequestForm url.Values,
|
wantRequestForm url.Values,
|
||||||
|
wantRequestedScopes []string,
|
||||||
wantGrantedOpenidScope bool,
|
wantGrantedOpenidScope bool,
|
||||||
|
wantGrantedOfflineAccessScope bool,
|
||||||
wantAccessTokenExpiresAt bool,
|
wantAccessTokenExpiresAt bool,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Assert that the getters on the authRequest return what we think they should.
|
// Assert that the getters on the request return what we think they should.
|
||||||
wantRequestedScopes := []string{"profile", "email"}
|
require.NotEmpty(t, request.GetID())
|
||||||
wantGrantedScopes := []string{}
|
testutil.RequireTimeInDelta(t, request.GetRequestedAt(), time.Now().UTC(), timeComparisonFudgeSeconds*time.Second)
|
||||||
if wantGrantedOpenidScope {
|
require.Equal(t, goodClient, request.GetClient().GetID())
|
||||||
wantRequestedScopes = append([]string{"openid"}, wantRequestedScopes...)
|
require.Equal(t, fosite.Arguments(wantRequestedScopes), request.GetRequestedScopes())
|
||||||
wantGrantedScopes = append([]string{"openid"}, wantGrantedScopes...)
|
require.Equal(t, fosite.Arguments(wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope)), request.GetGrantedScopes())
|
||||||
}
|
require.Empty(t, request.GetRequestedAudience())
|
||||||
require.NotEmpty(t, authRequest.GetID())
|
require.Empty(t, request.GetGrantedAudience())
|
||||||
testutil.RequireTimeInDelta(t, authRequest.GetRequestedAt(), time.Now().UTC(), timeComparisonFudgeSeconds*time.Second)
|
require.Equal(t, wantRequestForm, request.GetRequestForm()) // Fosite stores access token request without form
|
||||||
require.Equal(t, goodClient, authRequest.GetClient().GetID())
|
|
||||||
require.Equal(t, fosite.Arguments(wantRequestedScopes), authRequest.GetRequestedScopes())
|
|
||||||
require.Equal(t, fosite.Arguments(wantGrantedScopes), authRequest.GetGrantedScopes())
|
|
||||||
require.Empty(t, authRequest.GetRequestedAudience())
|
|
||||||
require.Empty(t, authRequest.GetGrantedAudience())
|
|
||||||
require.Equal(t, wantRequestForm, authRequest.GetRequestForm()) // Fosite stores access token request without form
|
|
||||||
|
|
||||||
// Cast session to the type we think it should be.
|
// Cast session to the type we think it should be.
|
||||||
session, ok := authRequest.GetSession().(*openid.DefaultSession)
|
session, ok := request.GetSession().(*openid.DefaultSession)
|
||||||
require.Truef(t, ok, "could not cast %T to %T", authRequest.GetSession(), &openid.DefaultSession{})
|
require.Truef(t, ok, "could not cast %T to %T", request.GetSession(), &openid.DefaultSession{})
|
||||||
|
|
||||||
// Assert that the session claims are what we think they should be, but only if we are doing OIDC.
|
// Assert that the session claims are what we think they should be, but only if we are doing OIDC.
|
||||||
if wantGrantedOpenidScope {
|
if wantGrantedOpenidScope {
|
||||||
|
@ -174,7 +174,7 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
|||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
listenAddr: "localhost:0",
|
listenAddr: "localhost:0",
|
||||||
scopes: []string{"offline_access", "openid", "email", "profile"},
|
scopes: []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "email", "profile"},
|
||||||
cache: &nopCache{},
|
cache: &nopCache{},
|
||||||
callbackPath: "/callback",
|
callbackPath: "/callback",
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
Loading…
Reference in New Issue
Block a user