authorize and callback endpoints now handle the offline_access scope

- This is in preparation for the token endpoint to support the refresh
  grant

Signed-off-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Aram Price 2020-12-07 17:22:34 -08:00 committed by Ryan Richard
parent 6a90a10123
commit d91baba240
6 changed files with 83 additions and 36 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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{},

View File

@ -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)

View File

@ -28,7 +28,7 @@ func TestNullStorage_GetClient(t *testing.T) {
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"},
Scopes: []string{"openid", "profile", "email"}, Scopes: []string{"openid", "offline_access", "profile", "email"},
}, },
TokenEndpointAuthMethod: "none", TokenEndpointAuthMethod: "none",
}, },

View File

@ -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"}, GrantTypes: []string{"authorization_code"},
Scopes: []string{"openid", "profile", "email"}, Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email"},
}, },
TokenEndpointAuthMethod: "none", TokenEndpointAuthMethod: "none",
} }
@ -156,3 +157,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)
}
}
}