Supervisor authorize endpoint errors when missing PKCE params

Signed-off-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Andrew Keesler 2020-11-04 12:19:07 -08:00 committed by Ryan Richard
parent 0045ce4286
commit 2564d1be42
2 changed files with 110 additions and 32 deletions

View File

@ -8,6 +8,8 @@ import (
"fmt"
"net/http"
"github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/compose"
"golang.org/x/oauth2"
"k8s.io/klog/v2"
@ -31,10 +33,14 @@ func NewHandler(
generatePKCE func() (pkce.Code, error),
generateNonce func() (nonce.Nonce, error),
) http.Handler {
oauthConfig := &compose.Config{
EnforcePKCEForPublicClients: true,
}
secret := []byte("some-cool-secret-that-is-32bytes") // TODO use a real secret once we care about real authorization codes
oauthHelper := compose.Compose(
// Empty Config for right now since we aren't using anything in it. We may want to inject this
// in the future since it has some really nice configuration knobs like token lifetime.
&compose.Config{},
oauthConfig,
// This is the thing that matters right now - the store is used to get information about the
// client in the authorization request.
@ -42,16 +48,18 @@ func NewHandler(
// Shouldn't need any of this filled in as of right now - we aren't doing auth code stuff,
// issuing ID tokens, or signing anything yet.
&compose.CommonStrategy{},
&compose.CommonStrategy{
CoreStrategy: compose.NewOAuth2HMACStrategy(oauthConfig, secret, nil),
},
// hasher, shouldn't need this right now - we aren't doing any client auth...yet?
nil,
// We will _probably_ want the below handlers somewhere in the code, but I'm not sure where yet,
// and we don't need them for the tests to pass currently, so they are commented out.
// compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2AuthorizeExplicitFactory,
// compose.OpenIDConnectExplicitFactory,
// compose.OAuth2PKCEFactory,
compose.OAuth2PKCEFactory,
)
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodPost && r.Method != http.MethodGet {
@ -70,6 +78,17 @@ func NewHandler(
return nil
}
// Perform validations
_, err = oauthHelper.NewAuthorizeResponse(
r.Context(), // TODO: maybe another context here since this one will expire?
authorizeRequester,
&openid.DefaultSession{},
)
if err != nil {
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
}
upstreamIDP, err := chooseUpstreamIDP(idpListGetter)
if err != nil {
return err

View File

@ -5,6 +5,7 @@ package auth
import (
"fmt"
"html"
"mime"
"net/http"
"net/http/httptest"
@ -49,6 +50,20 @@ func TestAuthorizationEndpoint(t *testing.T) {
}
`)
fositeMissingCodeChallengeErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed\n\nThis client must include a code_challenge when performing the authorize code flow, but it is missing.",
"error_hint": "This client must include a code_challenge when performing the authorize code flow, but it is missing.",
"state": "some-state-value",
}
fositeMissingCodeChallengeMethodErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed\n\nClients must use code_challenge_method=S256, plain is not allowed.",
"error_hint": "Clients must use code_challenge_method=S256, plain is not allowed.",
"state": "some-state-value",
}
fositeUnsupportedResponseTypeErrorQuery = map[string]string{
"error": "unsupported_response_type",
"error_description": "The authorization server does not support obtaining a token using this method\n\nThe client is not allowed to request response_type \"unsupported\".",
@ -103,6 +118,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
},
},
},
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
PKCES: map[string]fosite.Requester{},
}
happyStateGenerator := func() (state.State, error) { return "test-state", nil }
@ -111,7 +128,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
expectedCodeChallenge := "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"
expectedUpstreamCodeChallenge := "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"
pathWithQuery := func(path string, query map[string]string) string {
values := url.Values{}
@ -135,12 +152,14 @@ func TestAuthorizationEndpoint(t *testing.T) {
}
happyGetRequestQueryMap := map[string]string{
"response_type": "code",
"scope": "openid profile email",
"client_id": "pinniped-cli",
"state": "some-state-value",
"nonce": "some-nonce-value",
"redirect_uri": downstreamRedirectURI,
"response_type": "code",
"scope": "openid profile email",
"client_id": "pinniped-cli",
"state": "some-state-value",
"nonce": "some-nonce-value",
"code_challenge": "some-challenge",
"code_challenge_method": "S256",
"redirect_uri": downstreamRedirectURI,
}
happyGetRequestPath := pathWithQuery("/some/path", happyGetRequestQueryMap)
@ -169,7 +188,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
"client_id": "some-client-id",
"state": "test-state",
"nonce": "test-nonce",
"code_challenge": expectedCodeChallenge,
"code_challenge": expectedUpstreamCodeChallenge,
"code_challenge_method": "S256",
"redirect_uri": issuer + "/callback/some-idp",
},
@ -197,17 +216,20 @@ func TestAuthorizationEndpoint(t *testing.T) {
tests := []testCase{
{
name: "happy path using GET",
issuer: issuer,
idpListGetter: newIDPListGetter(upstreamOIDCIdentityProvider),
generateState: happyStateGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
method: http.MethodGet,
path: happyGetRequestPath,
wantStatus: http.StatusFound,
wantContentType: "text/html; charset=utf-8",
wantBodyString: "",
name: "happy path using GET",
issuer: issuer,
idpListGetter: newIDPListGetter(upstreamOIDCIdentityProvider),
generateState: happyStateGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
method: http.MethodGet,
path: happyGetRequestPath,
wantStatus: http.StatusFound,
wantContentType: "text/html; charset=utf-8",
wantBodyString: fmt.Sprintf(`<a href="%s">Found</a>.%s`,
html.EscapeString(happyGetRequestExpectedRedirectLocation),
"\n\n",
),
wantLocationHeader: happyGetRequestExpectedRedirectLocation,
},
{
@ -221,11 +243,13 @@ func TestAuthorizationEndpoint(t *testing.T) {
path: "/some/path",
contentType: "application/x-www-form-urlencoded",
body: url.Values{
"response_type": []string{"code"},
"scope": []string{"openid profile email"},
"client_id": []string{"pinniped-cli"},
"state": []string{"some-state-value"},
"redirect_uri": []string{downstreamRedirectURI},
"response_type": []string{"code"},
"scope": []string{"openid profile email"},
"client_id": []string{"pinniped-cli"},
"state": []string{"some-state-value"},
"code_challenge": []string{"some-challenge"},
"code_challenge_method": []string{"S256"},
"redirect_uri": []string{downstreamRedirectURI},
}.Encode(),
wantStatus: http.StatusFound,
wantContentType: "",
@ -272,6 +296,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "downstream scopes do not match what is configured for client",
@ -285,6 +310,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "",
},
{
name: "missing response type in request",
@ -298,6 +324,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "missing client id in request",
@ -312,6 +339,34 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: "application/json; charset=utf-8",
wantBodyJSON: fositeInvalidClientErrorBody,
},
{
name: "missing PKCE code_challenge in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
issuer: issuer,
idpListGetter: newIDPListGetter(upstreamOIDCIdentityProvider),
generateState: happyStateGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "",
},
{
name: "missing PKCE code_challenge_method in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1
issuer: issuer,
idpListGetter: newIDPListGetter(upstreamOIDCIdentityProvider),
generateState: happyStateGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "",
},
{
name: "state does not have enough entropy",
issuer: issuer,
@ -324,6 +379,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8",
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "",
},
{
name: "error while generating state",
@ -428,11 +484,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
require.Equal(t, test.wantStatus, rsp.Code)
requireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType)
if test.wantBodyString != "" {
require.Equal(t, test.wantBodyString, rsp.Body.String())
}
if test.wantBodyJSON != "" {
require.JSONEq(t, test.wantBodyJSON, rsp.Body.String())
} else {
require.Equal(t, test.wantBodyString, rsp.Body.String())
}
if test.wantLocationHeader != "" {
@ -475,11 +530,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
"client_id": "some-other-client-id",
"state": "test-state",
"nonce": "test-nonce",
"code_challenge": expectedCodeChallenge,
"code_challenge": expectedUpstreamCodeChallenge,
"code_challenge_method": "S256",
"redirect_uri": issuer + "/callback/some-other-idp",
},
)
test.wantBodyString = fmt.Sprintf(`<a href="%s">Found</a>.%s`,
html.EscapeString(test.wantLocationHeader),
"\n\n",
)
// Run again on the same instance of the subject with the modified upstream IDP settings and the
// modified expectations. This should ensure that the implementation is using the in-memory cache