Login page styling/structure for users, screen readers, passwd managers

Also:
- Add CSS to login page
- Refactor login page HTML and CSS into a new package
- New custom CSP headers for the login page, because the requirements
  are different from the form_post page
This commit is contained in:
Ryan Richard
2022-05-05 13:13:25 -07:00
parent 6ca7c932ae
commit cffa353ffb
18 changed files with 449 additions and 173 deletions
+16 -4
View File
@@ -54,7 +54,7 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret
require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets)
}
func RequireSecurityHeadersWithFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) {
func RequireSecurityHeadersWithFormPostPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Loosely confirm that the unique CSPs needed for the form_post page were used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "script-src '") // loose assertion
@@ -66,8 +66,20 @@ func RequireSecurityHeadersWithFormPostCSPs(t *testing.T, response *httptest.Res
requireSecurityHeaders(t, response)
}
func RequireSecurityHeadersWithoutFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Confirm that the unique CSPs needed for the form_post page were NOT used.
func RequireSecurityHeadersWithLoginPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Loosely confirm that the unique CSPs needed for the login page were used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "style-src '") // loose assertion
require.NotContains(t, cspHeader, "script-src") // only needed by form_post page
require.NotContains(t, cspHeader, "img-src data:") // only needed by form_post page
require.NotContains(t, cspHeader, "connect-src *") // only needed by form_post page
// Also require all the usual security headers.
requireSecurityHeaders(t, response)
}
func RequireSecurityHeadersWithoutCustomCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Confirm that the unique CSPs needed for the form_post or login page were NOT used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.NotContains(t, cspHeader, "script-src")
require.NotContains(t, cspHeader, "style-src")
@@ -79,7 +91,7 @@ func RequireSecurityHeadersWithoutFormPostCSPs(t *testing.T, response *httptest.
}
func requireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) {
// Loosely confirm that the generic CSPs were used.
// Loosely confirm that the generic default CSPs were used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "default-src 'none'")
require.Contains(t, cspHeader, "frame-ancestors 'none'")
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testutil
import (
"fmt"
"go.pinniped.dev/internal/here"
)
func ExpectedLoginPageHTML(wantCSS, wantIDPName, wantPostPath, wantEncodedState, wantAlert string) string {
alertHTML := ""
if wantAlert != "" {
alertHTML = fmt.Sprintf("\n"+
" <div class=\"form-field\">\n"+
" <span class=\"alert\" role=\"alert\" aria-label=\"login error message\">%s</span>\n"+
" </div>\n ",
wantAlert,
)
}
// Note that "role", "aria-*", and "alert" attributes are hints to screen readers.
// Also note that some structure and attributes used here are hints to password managers,
// see https://support.1password.com/compatible-website-design/.
// Please take care when changing the HTML of this form,
// and test with a screen reader and password manager after changes.
return here.Docf(`<!DOCTYPE html>
<html lang="en">
<head>
<title>Pinniped</title>
<meta charset="UTF-8">
<style>%s</style>
<link id="favicon" rel="icon"/>
</head>
<body>
<div class="box" aria-label="login form" role="main">
<div class="form-field">
<h1>Log in to %s</h1>
</div>
%s
<form action="%s" method="post">
<input type="hidden" name="state" id="state" value="%s">
<div class="form-field">
<label for="username"><span class="hidden" aria-hidden="true">Username</span></label>
<input type="text" name="username" id="username"
autocomplete="username" placeholder="Username" required>
</div>
<div class="form-field">
<label for="password"><span class="hidden" aria-hidden="true">Password</span></label>
<input type="password" name="password" id="password"
autocomplete="current-password" placeholder="Password" required>
</div>
<div class="form-field">
<input type="submit" name="submit" id="submit" value="Log in"/>
</div>
</form>
</div>
</body>
</html>
`,
wantCSS,
wantIDPName,
alertHTML,
wantPostPath,
wantEncodedState,
)
}