Supervisor authorize endpoint errors when missing PKCE params
Signed-off-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
parent
0045ce4286
commit
2564d1be42
@ -8,6 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
|
||||||
"github.com/ory/fosite/compose"
|
"github.com/ory/fosite/compose"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
@ -31,10 +33,14 @@ func NewHandler(
|
|||||||
generatePKCE func() (pkce.Code, error),
|
generatePKCE func() (pkce.Code, error),
|
||||||
generateNonce func() (nonce.Nonce, error),
|
generateNonce func() (nonce.Nonce, error),
|
||||||
) http.Handler {
|
) 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(
|
oauthHelper := compose.Compose(
|
||||||
// Empty Config for right now since we aren't using anything in it. We may want to inject this
|
// 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.
|
// 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
|
// This is the thing that matters right now - the store is used to get information about the
|
||||||
// client in the authorization request.
|
// 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,
|
// 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.
|
// 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?
|
// hasher, shouldn't need this right now - we aren't doing any client auth...yet?
|
||||||
nil,
|
nil,
|
||||||
|
|
||||||
// We will _probably_ want the below handlers somewhere in the code, but I'm not sure where yet,
|
// 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.
|
// and we don't need them for the tests to pass currently, so they are commented out.
|
||||||
// compose.OAuth2AuthorizeExplicitFactory,
|
compose.OAuth2AuthorizeExplicitFactory,
|
||||||
// compose.OpenIDConnectExplicitFactory,
|
// compose.OpenIDConnectExplicitFactory,
|
||||||
// compose.OAuth2PKCEFactory,
|
compose.OAuth2PKCEFactory,
|
||||||
)
|
)
|
||||||
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
if r.Method != http.MethodPost && r.Method != http.MethodGet {
|
if r.Method != http.MethodPost && r.Method != http.MethodGet {
|
||||||
@ -70,6 +78,17 @@ func NewHandler(
|
|||||||
return nil
|
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)
|
upstreamIDP, err := chooseUpstreamIDP(idpListGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -5,6 +5,7 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"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{
|
fositeUnsupportedResponseTypeErrorQuery = map[string]string{
|
||||||
"error": "unsupported_response_type",
|
"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\".",
|
"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 }
|
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:
|
// 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
|
// $ 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 {
|
pathWithQuery := func(path string, query map[string]string) string {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
@ -140,6 +157,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"client_id": "pinniped-cli",
|
"client_id": "pinniped-cli",
|
||||||
"state": "some-state-value",
|
"state": "some-state-value",
|
||||||
"nonce": "some-nonce-value",
|
"nonce": "some-nonce-value",
|
||||||
|
"code_challenge": "some-challenge",
|
||||||
|
"code_challenge_method": "S256",
|
||||||
"redirect_uri": downstreamRedirectURI,
|
"redirect_uri": downstreamRedirectURI,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +188,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"client_id": "some-client-id",
|
"client_id": "some-client-id",
|
||||||
"state": "test-state",
|
"state": "test-state",
|
||||||
"nonce": "test-nonce",
|
"nonce": "test-nonce",
|
||||||
"code_challenge": expectedCodeChallenge,
|
"code_challenge": expectedUpstreamCodeChallenge,
|
||||||
"code_challenge_method": "S256",
|
"code_challenge_method": "S256",
|
||||||
"redirect_uri": issuer + "/callback/some-idp",
|
"redirect_uri": issuer + "/callback/some-idp",
|
||||||
},
|
},
|
||||||
@ -207,7 +226,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "text/html; charset=utf-8",
|
wantContentType: "text/html; charset=utf-8",
|
||||||
wantBodyString: "",
|
wantBodyString: fmt.Sprintf(`<a href="%s">Found</a>.%s`,
|
||||||
|
html.EscapeString(happyGetRequestExpectedRedirectLocation),
|
||||||
|
"\n\n",
|
||||||
|
),
|
||||||
wantLocationHeader: happyGetRequestExpectedRedirectLocation,
|
wantLocationHeader: happyGetRequestExpectedRedirectLocation,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -225,6 +247,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"scope": []string{"openid profile email"},
|
"scope": []string{"openid profile email"},
|
||||||
"client_id": []string{"pinniped-cli"},
|
"client_id": []string{"pinniped-cli"},
|
||||||
"state": []string{"some-state-value"},
|
"state": []string{"some-state-value"},
|
||||||
|
"code_challenge": []string{"some-challenge"},
|
||||||
|
"code_challenge_method": []string{"S256"},
|
||||||
"redirect_uri": []string{downstreamRedirectURI},
|
"redirect_uri": []string{downstreamRedirectURI},
|
||||||
}.Encode(),
|
}.Encode(),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
@ -272,6 +296,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "downstream scopes do not match what is configured for client",
|
name: "downstream scopes do not match what is configured for client",
|
||||||
@ -285,6 +310,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing response type in request",
|
name: "missing response type in request",
|
||||||
@ -298,6 +324,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing client id in request",
|
name: "missing client id in request",
|
||||||
@ -312,6 +339,34 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantBodyJSON: fositeInvalidClientErrorBody,
|
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",
|
name: "state does not have enough entropy",
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
@ -324,6 +379,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error while generating state",
|
name: "error while generating state",
|
||||||
@ -428,11 +484,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
require.Equal(t, test.wantStatus, rsp.Code)
|
require.Equal(t, test.wantStatus, rsp.Code)
|
||||||
requireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType)
|
requireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType)
|
||||||
|
|
||||||
if test.wantBodyString != "" {
|
|
||||||
require.Equal(t, test.wantBodyString, rsp.Body.String())
|
|
||||||
}
|
|
||||||
if test.wantBodyJSON != "" {
|
if test.wantBodyJSON != "" {
|
||||||
require.JSONEq(t, test.wantBodyJSON, rsp.Body.String())
|
require.JSONEq(t, test.wantBodyJSON, rsp.Body.String())
|
||||||
|
} else {
|
||||||
|
require.Equal(t, test.wantBodyString, rsp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if test.wantLocationHeader != "" {
|
if test.wantLocationHeader != "" {
|
||||||
@ -475,11 +530,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"client_id": "some-other-client-id",
|
"client_id": "some-other-client-id",
|
||||||
"state": "test-state",
|
"state": "test-state",
|
||||||
"nonce": "test-nonce",
|
"nonce": "test-nonce",
|
||||||
"code_challenge": expectedCodeChallenge,
|
"code_challenge": expectedUpstreamCodeChallenge,
|
||||||
"code_challenge_method": "S256",
|
"code_challenge_method": "S256",
|
||||||
"redirect_uri": issuer + "/callback/some-other-idp",
|
"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
|
// 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
|
// modified expectations. This should ensure that the implementation is using the in-memory cache
|
||||||
|
Loading…
Reference in New Issue
Block a user