<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Secrets on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/secrets/</link><description>Recent content in Secrets on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Tue, 18 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/secrets/index.xml" rel="self" type="application/rss+xml"/><item><title>The 401 that never got to happen</title><link>https://phpboyscout.uk/the-401-that-never-got-to-happen/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/the-401-that-never-got-to-happen/</guid><description>&lt;img src="https://phpboyscout.uk/the-401-that-never-got-to-happen/cover-the-401-that-never-got-to-happen.png" alt="Featured image of post The 401 that never got to happen" /&gt;&lt;p&gt;I was adding auth to the keryx studio, the little browser UI where I review a reel&amp;rsquo;s takes before anything gets posted. The plan was the obvious one: the server mints a random bearer token at startup, prints it once, and every request has to carry it. My own framework already had the middleware. Ten minutes of wiring, tops. Then I read the middleware before wiring it, and stopped.&lt;/p&gt;
&lt;h2 id="the-header-a-browser-refuses-to-send"&gt;The header a browser refuses to send
&lt;/h2&gt;&lt;p&gt;Bearer tokens live in the &lt;code&gt;Authorization&lt;/code&gt; header, and the middleware in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base" target="_blank" rel="noopener"
 &gt;go-tool-base&lt;/a&gt; extracts credentials from headers. Which is fine for &lt;code&gt;fetch()&lt;/code&gt; calls, the JavaScript can attach whatever it likes. But not everything the studio loads goes through &lt;code&gt;fetch()&lt;/code&gt;. It&amp;rsquo;s a page full of &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tags, every take and preview pulled straight from a &lt;code&gt;src&lt;/code&gt; attribute pointing at &lt;code&gt;/api/v1/workspace/{slug}/file/...&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And a browser will not attach your Authorization header to an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag. There&amp;rsquo;s no attribute for it, no polite workaround; that&amp;rsquo;s simply not how &lt;code&gt;src&lt;/code&gt; fetches work. And those media URLs live under &lt;code&gt;/api/&lt;/code&gt;, so the bearer middleware gates them along with everything else: the &lt;code&gt;fetch()&lt;/code&gt; calls carry their token and work, the page shell loads, and every single image comes back 401. A studio for reviewing your media that shows you exactly none of it. Nice.&lt;/p&gt;
&lt;p&gt;The satisfying part: this never actually happened. No broken page, no debugging session at midnight. It fell out of reading the dependency&amp;rsquo;s API before writing the first line against it, which sounds virtuous and mostly means I&amp;rsquo;ve been burned before.&lt;/p&gt;
&lt;h2 id="two-fixes-i-didnt-like"&gt;Two fixes I didn&amp;rsquo;t like
&lt;/h2&gt;&lt;p&gt;The obvious patches were sitting right there. Option one: studio-local glue, some cookie-or-query-string handling bolted into keryx in front of the framework&amp;rsquo;s middleware. Option two: append &lt;code&gt;?token=...&lt;/code&gt; to every media URL the studio renders.&lt;/p&gt;
&lt;p&gt;Neither survived contact. The query-string version smears the credential across every URL, where it leaks into logs and copy-pasted links (a token in a URL is a &lt;a class="link" href="https://phpboyscout.uk/who-holds-the-client-secret/" &gt;secret with a publicist&lt;/a&gt;). And the app-local glue means my application grows its own private auth layer in front of the framework&amp;rsquo;s auth layer, which is two places for bugs to disagree with each other.&lt;/p&gt;
&lt;p&gt;So I went with the third option, the one that wasn&amp;rsquo;t on the list: if the framework&amp;rsquo;s middleware only understands headers, teach the framework about cookies. It&amp;rsquo;s my framework. That&amp;rsquo;s the whole point of &lt;a class="link" href="https://phpboyscout.uk/a-feature-request-into-my-own-framework/" &gt;owning the thing&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="a-cookie-the-framework-understands"&gt;A cookie the framework understands
&lt;/h2&gt;&lt;p&gt;go-tool-base v0.24.0 gained a cookie verifier as a first-class option on the same middleware:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;WithCookieVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;authn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Verifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AuthOption&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cookieName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/e38f9d8/pkg/http/auth.go#L42-53" target="_blank" rel="noopener"
 &gt;pkged at v0.24.0&lt;/a&gt;.) The cookie is deliberately the &lt;em&gt;ambient&lt;/em&gt; credential: if a request carries an explicit Authorization header, the header always wins. Cookies ride along on &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; fetches for free, because riding along on requests is the one thing cookies have always done. The keryx side then composes it like any other option:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;authMW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithBearerVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCookieVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionCookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAuthSkipper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/api/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithAuthLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;a class="link" href="https://gitlab.com/phpboyscout/keryx/-/blob/e3f2bf1/pkg/studio/auth.go#L80-85" target="_blank" rel="noopener"
 &gt;keryx at e3f2bf1&lt;/a&gt;.) The token itself is 32 bytes from &lt;code&gt;crypto/rand&lt;/code&gt;, minted per run, never written to disk. The cookie is HttpOnly and SameSite=Strict, though not Secure, because this runs over plain http on a LAN and I&amp;rsquo;d rather document that limitation than pretend TLS exists where it doesn&amp;rsquo;t. Localhost stays open, only &lt;code&gt;/api/*&lt;/code&gt; is gated, and if the gate can&amp;rsquo;t be built at startup the server refuses to start at all. No gate, no server&amp;hellip; I&amp;rsquo;d sooner it fell over at boot than came up with the door propped open.&lt;/p&gt;
&lt;h2 id="the-only-401-fired-on-purpose"&gt;The only 401 fired on purpose
&lt;/h2&gt;&lt;p&gt;The end-to-end test binds the studio to a non-loopback address, hits the API without the token, and asserts the 401 actually fires; then again with the token, asserting the 200. The failure mode I spotted in a spec review now exists in exactly one place, deliberately, in a test, proving the gate is real.&lt;/p&gt;
&lt;p&gt;No grand moral here, just a small habit worth naming: the ten minutes spent reading &lt;code&gt;AuthMiddleware&lt;/code&gt;&amp;rsquo;s source before calling it is the cheapest debugging session I&amp;rsquo;ve ever had. The alternative timeline, the one where I wire it up first, is me squinting at a page of broken image icons wondering what on earth I&amp;rsquo;d broken. And the answer, of course, would have been that I hadn&amp;rsquo;t broken a thing. The code was fine. The whole design was wrong.&lt;/p&gt;
&lt;p&gt;I prefer this timeline. The images load, the gate holds, and the framework walked away with a feature every future tool of mine gets for free.&lt;/p&gt;</description></item></channel></rss>