Add unit test for rendering form_post response from POST /login

This commit is contained in:
Ryan Richard 2022-05-04 12:12:14 -07:00
parent 329d41aac7
commit 6ca7c932ae
2 changed files with 57 additions and 8 deletions

View File

@ -84,15 +84,11 @@ func NewHandler(
func wrapSecurityHeaders(handler http.Handler) http.Handler { func wrapSecurityHeaders(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var wrapped http.Handler wrapped := securityheader.Wrap(handler)
switch r.Method { if r.Method == http.MethodPost {
case http.MethodPost:
// POST requests can result in the form_post html page, so allow it with CSP headers. // POST requests can result in the form_post html page, so allow it with CSP headers.
wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy()) wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
default:
wrapped = securityheader.Wrap(handler)
} }
wrapped.ServeHTTP(w, r) wrapped.ServeHTTP(w, r)
}) })
} }

View File

@ -249,6 +249,7 @@ func TestPostLoginEndpoint(t *testing.T) {
// upstream LDAP or AD provider. // upstream LDAP or AD provider.
wantRedirectLocationRegexp string // for loose matching wantRedirectLocationRegexp string // for loose matching
wantRedirectLocationString string // for exact matching instead wantRedirectLocationString string // for exact matching instead
wantBodyFormResponseRegexp string // for form_post html page matching instead
wantDownstreamRedirectURI string wantDownstreamRedirectURI string
wantDownstreamGrantedScopes []string wantDownstreamGrantedScopes []string
wantDownstreamIDTokenSubject string wantDownstreamIDTokenSubject string
@ -311,6 +312,30 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
}, },
{
name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["response_mode"] = []string{"form_post"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBodyFormResponseRegexp: `(?s)<html.*<script>.*To finish logging in, paste this authorization code` +
`.*<form>.*<code id="manual-auth-code">(.+)</code>.*</html>`, // "(?s)" means match "." across newlines
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{ {
name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
@ -652,12 +677,13 @@ func TestPostLoginEndpoint(t *testing.T) {
require.Equal(t, tt.wantStatus, rsp.Code) require.Equal(t, tt.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType)
require.Equal(t, tt.wantBodyString, rsp.Body.String())
actualLocation := rsp.Header().Get("Location") actualLocation := rsp.Header().Get("Location")
switch { switch {
case tt.wantRedirectLocationRegexp != "": case tt.wantRedirectLocationRegexp != "":
// Expecting a success redirect to the client.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
require.Len(t, rsp.Header().Values("Location"), 1) require.Len(t, rsp.Header().Values("Location"), 1)
oidctestutil.RequireAuthCodeRegexpMatch( oidctestutil.RequireAuthCodeRegexpMatch(
t, t,
@ -679,15 +705,42 @@ func TestPostLoginEndpoint(t *testing.T) {
tt.wantDownstreamCustomSessionData, tt.wantDownstreamCustomSessionData,
) )
case tt.wantRedirectToLoginPageError != "": case tt.wantRedirectToLoginPageError != "":
// Expecting an error redirect to the login UI page.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath + expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath +
"?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState "?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require.Equal(t, expectedLocation, actualLocation) require.Equal(t, expectedLocation, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords)
case tt.wantRedirectLocationString != "": case tt.wantRedirectLocationString != "":
// Expecting an error redirect to the client.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
require.Equal(t, tt.wantRedirectLocationString, actualLocation) require.Equal(t, tt.wantRedirectLocationString, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords)
case tt.wantBodyFormResponseRegexp != "":
// Expecting the body of the response to be a html page with a form (for "response_mode=form_post").
_, hasLocationHeader := rsp.Header()["Location"]
require.False(t, hasLocationHeader)
oidctestutil.RequireAuthCodeRegexpMatch(
t,
rsp.Body.String(),
tt.wantBodyFormResponseRegexp,
kubeClient,
secretsClient,
kubeOauthStore,
tt.wantDownstreamGrantedScopes,
tt.wantDownstreamIDTokenSubject,
tt.wantDownstreamIDTokenUsername,
tt.wantDownstreamIDTokenGroups,
tt.wantDownstreamRequestedScopes,
tt.wantDownstreamPKCEChallenge,
tt.wantDownstreamPKCEChallengeMethod,
tt.wantDownstreamNonce,
downstreamClientID,
tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData,
)
default: default:
require.Failf(t, "test should have expected a redirect", require.Failf(t, "test should have expected a redirect or form body",
"actual location was %q", actualLocation) "actual location was %q", actualLocation)
} }
}) })