mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-06 16:17:08 +00:00
Add custom response_mode=form_post HTML template.
This is a new pacakge internal/oidc/provider/formposthtml containing a number of static files embedded using the relatively recent Go "//go:embed" functionality introduced in Go 1.16 (https://blog.golang.org/go1.16). The Javascript and CSS files are minifiied and injected to make a single self-contained HTML response. There is a special Content-Security-Policy helper to calculate hash-based script-src and style-src rules. This new code is covered by a new integration test that exercises the JS/HTML functionality in a real browser outside of the rest of the Supervisor. Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func NewHandler(
|
||||
stateDecoder, cookieDecoder oidc.Decoder,
|
||||
redirectURI string,
|
||||
) http.Handler {
|
||||
return securityheader.Wrap(httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
handler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
state, err := validateRequest(r, stateDecoder, cookieDecoder)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -97,7 +98,8 @@ func NewHandler(
|
||||
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
||||
|
||||
return nil
|
||||
}))
|
||||
})
|
||||
return securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
|
||||
}
|
||||
|
||||
func authcode(r *http.Request) string {
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
csrfCookie: happyCSRFCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html;charset=UTF-8",
|
||||
wantBodyFormResponseRegexp: `<input type="hidden" name="code" value="(.+)"/>`,
|
||||
wantBodyFormResponseRegexp: `<code id="manual-auth-code">(.+)</code>`,
|
||||
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject,
|
||||
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
"go.pinniped.dev/internal/oidc/jwks"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||
)
|
||||
@@ -217,7 +218,7 @@ func FositeOauth2Helper(
|
||||
MinParameterEntropy: fosite.MinParameterEntropy,
|
||||
}
|
||||
|
||||
return compose.Compose(
|
||||
provider := compose.Compose(
|
||||
oauthConfig,
|
||||
oauthStore,
|
||||
&compose.CommonStrategy{
|
||||
@@ -233,6 +234,8 @@ func FositeOauth2Helper(
|
||||
compose.OAuth2PKCEFactory,
|
||||
TokenExchangeFactory,
|
||||
)
|
||||
provider.(*fosite.Fosite).FormPostHTMLTemplate = formposthtml.Template()
|
||||
return provider
|
||||
}
|
||||
|
||||
// FositeErrorForLog generates a list of information about the provided Fosite error that can be
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* Copyright 2021 the Pinniped contributors. All Rights Reserved. */
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
body {
|
||||
font-family: "Metropolis-Light", Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.state {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
width: 400px;
|
||||
height: 80px;
|
||||
margin-top: -40px;
|
||||
margin-left: -200px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: -10px;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
display: inline;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all .1s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #eee;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #ddd;
|
||||
transform: scale(.99);
|
||||
}
|
||||
|
||||
code {
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
hyphenate-character: '';
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
float: left;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding-top: 2px;
|
||||
padding-right: 10px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
/*
|
||||
This is the "copy-to-clipboard-line.svg" icon from Clarity (https://clarity.design/):
|
||||
https://github.com/vmware/clarity-assets/blob/master/icons/essential/copy-to-clipboard-line.svg
|
||||
*/
|
||||
background-image: url("data:image/svg+xml,%3Csvg version='1.1' width='36' height='36' viewBox='0 0 36 36' preserveAspectRatio='xMidYMid meet' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Ecopy-to-clipboard-line%3C/title%3E%3Cpath d='M22.6,4H21.55a3.89,3.89,0,0,0-7.31,0H13.4A2.41,2.41,0,0,0,11,6.4V10H25V6.4A2.41,2.41,0,0,0,22.6,4ZM23,8H13V6.25A.25.25,0,0,1,13.25,6h2.69l.12-1.11A1.24,1.24,0,0,1,16.61,4a2,2,0,0,1,3.15,1.18l.09.84h2.9a.25.25,0,0,1,.25.25Z' class='clr-i-outline clr-i-outline-path-1'%3E%3C/path%3E%3Cpath d='M33.25,18.06H21.33l2.84-2.83a1,1,0,1,0-1.42-1.42L17.5,19.06l5.25,5.25a1,1,0,0,0,.71.29,1,1,0,0,0,.71-1.7l-2.84-2.84H33.25a1,1,0,0,0,0-2Z' class='clr-i-outline clr-i-outline-path-2'%3E%3C/path%3E%3Cpath d='M29,16h2V6.68A1.66,1.66,0,0,0,29.35,5H27.08V7H29Z' class='clr-i-outline clr-i-outline-path-3'%3E%3C/path%3E%3Cpath d='M29,31H7V7H9V5H6.64A1.66,1.66,0,0,0,5,6.67V31.32A1.66,1.66,0,0,0,6.65,33H29.36A1.66,1.66,0,0,0,31,31.33V22.06H29Z' class='clr-i-outline clr-i-outline-path-4'%3E%3C/path%3E%3Crect x='0' y='0' width='36' height='36' fill-opacity='0'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#loading {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: -40px;
|
||||
margin-left: -40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
border-top-color: #1b3951;
|
||||
animation: loader .6s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
--><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{{ minifiedCSS }}</style>
|
||||
<script>{{ minifiedJS }}</script>
|
||||
<link id="favicon" rel="icon"/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
To finish logging in, paste this authorization code into your command-line session: {{ .Parameters.Get "code" }}
|
||||
</noscript>
|
||||
<form>
|
||||
<input type="hidden" name="redirect_uri" value="{{ .RedirURL }}"/>
|
||||
<input type="hidden" name="encoded_params" value="{{ .Parameters.Encode }}"/>
|
||||
</form>
|
||||
<div id="loading" class="state" data-favicon="⏳" data-title="Logging in..." hidden></div>
|
||||
<div id="success" class="state" data-favicon="✅" data-title="Login succeeded" hidden>
|
||||
<h1>Login succeeded</h1>
|
||||
<p>You have successfully logged in. You may now close this tab.</p>
|
||||
</div>
|
||||
<div id="manual" class="state" data-favicon="⌛" data-title="Finish your login" hidden>
|
||||
<h1>Finish your login</h1>
|
||||
<p>To finish logging in, paste this authorization code into your command-line session:</p>
|
||||
<button id="manual-copy-button">
|
||||
<span class="copy-icon"></span>
|
||||
<code id="manual-auth-code">{{ .Parameters.Get "code" }}</code>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
window.onload = () => {
|
||||
const transitionToState = (id) => {
|
||||
// Hide all the other ".state" <div>s.
|
||||
Array.from(document.querySelectorAll('.state')).forEach(e => e.hidden = true);
|
||||
|
||||
// Unhide the current state <div>.
|
||||
const currentDiv = document.getElementById(id)
|
||||
currentDiv.hidden = false;
|
||||
|
||||
// Set the window title.
|
||||
document.title = currentDiv.dataset.title;
|
||||
|
||||
// Set the favicon using inline SVG (does not work on Safari).
|
||||
document.getElementById('favicon').setAttribute(
|
||||
'href',
|
||||
'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>' +
|
||||
currentDiv.dataset.favicon +
|
||||
'</text></svg>'
|
||||
);
|
||||
}
|
||||
|
||||
// At load, show the spinner, hide the other divs, set the favicon, and
|
||||
// replace the URL path with './' so the upstream auth code disappears.
|
||||
transitionToState('loading');
|
||||
window.history.replaceState(null, '', './');
|
||||
|
||||
// When the copy button is clicked, copy to the clipboard.
|
||||
document.getElementById('manual-copy-button').onclick = () => {
|
||||
const code = document.getElementById('manual-copy-button').innerText;
|
||||
navigator.clipboard.writeText(code)
|
||||
.then(() => console.info('copied authorization code ' + code + ' to clipboard'))
|
||||
.catch(e => console.error('failed to copy code ' + code + ' to clipboard: ' + e));
|
||||
};
|
||||
|
||||
// Set a timeout to transition to the "manual" state if nothing succeeds within 2s.
|
||||
const timeout = setTimeout(() => transitionToState('manual'), 2000);
|
||||
|
||||
// Try to submit the POST callback, handling the success and error cases.
|
||||
const responseParams = document.forms[0].elements;
|
||||
fetch(
|
||||
responseParams['redirect_uri'].value,
|
||||
{
|
||||
method: 'POST',
|
||||
mode: 'no-cors',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
|
||||
body: responseParams['encoded_params'].value,
|
||||
})
|
||||
.then(() => clearTimeout(timeout))
|
||||
.then(() => transitionToState('success'))
|
||||
.catch(() => transitionToState('manual'));
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package formposthtml defines HTML templates used by the Supervisor.
|
||||
//nolint: gochecknoglobals // This package uses globals to ensure that all parsing and minifying happens at init.
|
||||
package formposthtml
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed" // Needed to trigger //go:embed directives below.
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/minify/v2/minify"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed form_post.css
|
||||
rawCSS string
|
||||
minifiedCSS = mustMinify(minify.CSS(rawCSS))
|
||||
|
||||
//go:embed form_post.js
|
||||
rawJS string
|
||||
minifiedJS = mustMinify(minify.JS(rawJS))
|
||||
|
||||
//go:embed form_post.gohtml
|
||||
rawHTMLTemplate string
|
||||
)
|
||||
|
||||
// Parse the Go templated HTML and inject functions providing the minified inline CSS and JS.
|
||||
var parsedHTMLTemplate = template.Must(template.New("form_post.gohtml").Funcs(template.FuncMap{
|
||||
"minifiedCSS": func() template.CSS { return template.CSS(minifiedCSS) },
|
||||
"minifiedJS": func() template.JS { return template.JS(minifiedJS) }, //nolint:gosec // This is 100% static input, not attacker-controlled.
|
||||
}).Parse(rawHTMLTemplate))
|
||||
|
||||
// Generate the CSP header value once since it's effectively constant:
|
||||
var cspValue = strings.Join([]string{
|
||||
`default-src 'none'`,
|
||||
`script-src '` + cspHash(minifiedJS) + `'`,
|
||||
`style-src '` + cspHash(minifiedCSS) + `'`,
|
||||
`img-src data:`,
|
||||
`connect-src *`,
|
||||
`frame-ancestors 'none'`,
|
||||
}, "; ")
|
||||
|
||||
func mustMinify(s string, err error) string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func cspHash(s string) string {
|
||||
hashBytes := sha256.Sum256([]byte(s))
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(hashBytes[:])
|
||||
}
|
||||
|
||||
// ContentSecurityPolicy returns the Content-Security-Policy header value to make the Template() operate correctly.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src#:~:text=%27%3Chash-algorithm%3E-%3Cbase64-value%3E%27.
|
||||
func ContentSecurityPolicy() string { return cspValue }
|
||||
|
||||
// Template returns the html/template.Template for rendering the response_type=form_post response page.
|
||||
func Template() *template.Template { return parsedHTMLTemplate }
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package formposthtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
var (
|
||||
testRedirectURL = "http://127.0.0.1:12345/callback"
|
||||
|
||||
testResponseParams = url.Values{
|
||||
"code": []string{"test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU"},
|
||||
"scope": []string{"openid offline_access pinniped:request-audience"},
|
||||
"state": []string{"01234567890123456789012345678901"},
|
||||
}
|
||||
|
||||
testExpectedFormPostOutput = here.Doc(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>body{font-family:metropolis-light,Helvetica,sans-serif}h1{font-size:20px}.state{position:absolute;top:100px;left:50%;width:400px;height:80px;margin-top:-40px;margin-left:-200px;font-size:14px;line-height:24px}button{margin:-10px;padding:10px;text-align:left;width:100%;display:inline;border:none;background:0 0;cursor:pointer;transition:all .1s}button:hover{background-color:#eee;transform:scale(1.01)}button:active{background-color:#ddd;transform:scale(.99)}code{word-wrap:break-word;hyphens:auto;hyphenate-character:'';font-size:12px;font-family:monospace;color:#333}.copy-icon{float:left;width:36px;height:36px;padding-top:2px;padding-right:10px;background-size:contain;background-repeat:no-repeat;background-image:url("data:image/svg+xml,%3Csvg width=%2236%22 height=%2236%22 viewBox=%220 0 36 36%22 xmlns=%22http://www.w3.org/2000/svg%22 xmlns:xlink=%22http://www.w3.org/1999/xlink%22%3E%3Ctitle%3Ecopy-to-clipboard-line%3C/title%3E%3Cpath d=%22M22.6 4H21.55a3.89 3.89.0 00-7.31.0H13.4A2.41 2.41.0 0011 6.4V10H25V6.4A2.41 2.41.0 0022.6 4zM23 8H13V6.25A.25.25.0 0113.25 6h2.69l.12-1.11A1.24 1.24.0 0116.61 4a2 2 0 013.15 1.18l.09.84h2.9a.25.25.0 01.25.25z%22 class=%22clr-i-outline clr-i-outline-path-1%22/%3E%3Cpath d=%22M33.25 18.06H21.33l2.84-2.83a1 1 0 10-1.42-1.42L17.5 19.06l5.25 5.25a1 1 0 00.71.29 1 1 0 00.71-1.7l-2.84-2.84H33.25a1 1 0 000-2z%22 class=%22clr-i-outline clr-i-outline-path-2%22/%3E%3Cpath d=%22M29 16h2V6.68A1.66 1.66.0 0029.35 5H27.08V7H29z%22 class=%22clr-i-outline clr-i-outline-path-3%22/%3E%3Cpath d=%22M29 31H7V7H9V5H6.64A1.66 1.66.0 005 6.67V31.32A1.66 1.66.0 006.65 33H29.36A1.66 1.66.0 0031 31.33V22.06H29z%22 class=%22clr-i-outline clr-i-outline-path-4%22/%3E%3Crect x=%220%22 y=%220%22 width=%2236%22 height=%2236%22 fill-opacity=%220%22/%3E%3C/svg%3E")}@keyframes loader{to{transform:rotate(360deg)}}#loading{content:'';box-sizing:border-box;width:80px;height:80px;margin-top:-40px;margin-left:-40px;border-radius:50%;border:2px solid #fff;border-top-color:#1b3951;animation:loader .6s linear infinite}</style>
|
||||
<script>window.onload=()=>{const a=b=>{Array.from(document.querySelectorAll('.state')).forEach(a=>a.hidden=!0);const a=document.getElementById(b);a.hidden=!1,document.title=a.dataset.title,document.getElementById('favicon').setAttribute('href','data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>'+a.dataset.favicon+'</text></svg>')};a('loading'),window.history.replaceState(null,'','./'),document.getElementById('manual-copy-button').onclick=()=>{const a=document.getElementById('manual-copy-button').innerText;navigator.clipboard.writeText(a).then(()=>console.info('copied authorization code '+a+' to clipboard')).catch(b=>console.error('failed to copy code '+a+' to clipboard: '+b))};const c=setTimeout(()=>a('manual'),2e3),b=document.forms[0].elements;fetch(b.redirect_uri.value,{method:'POST',mode:'no-cors',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:b.encoded_params.value}).then(()=>clearTimeout(c)).then(()=>a('success')).catch(()=>a('manual'))}</script>
|
||||
<link id="favicon" rel="icon"/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
To finish logging in, paste this authorization code into your command-line session: test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU
|
||||
</noscript>
|
||||
<form>
|
||||
<input type="hidden" name="redirect_uri" value="http://127.0.0.1:12345/callback"/>
|
||||
<input type="hidden" name="encoded_params" value="code=test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU&scope=openid+offline_access+pinniped%3Arequest-audience&state=01234567890123456789012345678901"/>
|
||||
</form>
|
||||
<div id="loading" class="state" data-favicon="⏳" data-title="Logging in..." hidden></div>
|
||||
<div id="success" class="state" data-favicon="✅" data-title="Login succeeded" hidden>
|
||||
<h1>Login succeeded</h1>
|
||||
<p>You have successfully logged in. You may now close this tab.</p>
|
||||
</div>
|
||||
<div id="manual" class="state" data-favicon="⌛" data-title="Finish your login" hidden>
|
||||
<h1>Finish your login</h1>
|
||||
<p>To finish logging in, paste this authorization code into your command-line session:</p>
|
||||
<button id="manual-copy-button">
|
||||
<span class="copy-icon"></span>
|
||||
<code id="manual-auth-code">test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU</code>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
// It's okay if this changes in the future, but this gives us a chance to eyeball the formatting.
|
||||
// Our browser-based integration tests should find any incompatibilities.
|
||||
testExpectedCSP = `default-src 'none'; ` +
|
||||
`script-src 'sha256-U+tKnJ2oMSYKSxmSX3V2mPBN8xdr9JpampKAhbSo108='; ` +
|
||||
`style-src 'sha256-TLAQE3UR2KpwP7AzMCE4iPDizh7zLPx9UXeK5ntuoRg='; ` +
|
||||
`img-src data:; ` +
|
||||
`connect-src *; ` +
|
||||
`frame-ancestors 'none'`
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
// Use the Fosite helper to render the form, ensuring that the parameters all have the same names + types.
|
||||
var buf bytes.Buffer
|
||||
fosite.WriteAuthorizeFormPostResponse(testRedirectURL, testResponseParams, Template(), &buf)
|
||||
|
||||
// Render again so we can confirm that there is no error returned (Fosite ignores any error).
|
||||
var buf2 bytes.Buffer
|
||||
require.NoError(t, Template().Execute(&buf2, struct {
|
||||
RedirURL string
|
||||
Parameters url.Values
|
||||
}{
|
||||
RedirURL: testRedirectURL,
|
||||
Parameters: testResponseParams,
|
||||
}))
|
||||
|
||||
require.Equal(t, buf.String(), buf2.String())
|
||||
require.Equal(t, testExpectedFormPostOutput, buf.String())
|
||||
}
|
||||
|
||||
func TestContentSecurityPolicyHashes(t *testing.T) {
|
||||
require.Equal(t, testExpectedCSP, ContentSecurityPolicy())
|
||||
}
|
||||
|
||||
func TestHelpers(t *testing.T) {
|
||||
// These are silly tests but it's easy to we might as well have them.
|
||||
require.Equal(t, "test", mustMinify("test", nil))
|
||||
require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) })
|
||||
|
||||
// Example test vector from https://content-security-policy.com/hash/.
|
||||
require.Equal(t, "sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=", cspHash("doSomething();"))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testutil
|
||||
@@ -55,7 +55,9 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret
|
||||
}
|
||||
|
||||
func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) {
|
||||
require.Equal(t, "default-src 'none'; frame-ancestors 'none'", response.Header().Get("Content-Security-Policy"))
|
||||
// This is a more relaxed assertion rather than an exact match, so it can cover all the CSP headers we use.
|
||||
require.Contains(t, response.Header().Get("Content-Security-Policy"), "default-src 'none'")
|
||||
|
||||
require.Equal(t, "DENY", response.Header().Get("X-Frame-Options"))
|
||||
require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection"))
|
||||
require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options"))
|
||||
|
||||
Reference in New Issue
Block a user