<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Oauth on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/oauth/</link><description>Recent content in Oauth on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.166.0</generator><language>en-us</language><lastBuildDate>Wed, 23 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/oauth/feed.xml" rel="self" type="application/rss+xml"/><item><title>Configuring identity-only OAuth requests in Waaseyaa</title><link>https://jonesrussell.github.io/blog/identity-only-oauth-requests/</link><pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/identity-only-oauth-requests/</guid><category>php</category><category>waaseyaa</category><blog:tag>php</blog:tag><blog:tag>waaseyaa</blog:tag><blog:tag>oauth</blog:tag><blog:tag>security</blog:tag><description>How Waaseyaa&amp;rsquo;s oauth-provider package added optional, additive configuration so a consumer that only needs a stable Google or GitHub identity can skip the email lookup, the forced consent prompt, and the offline refresh-token grant.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p><a href="https://github.com/waaseyaa/framework">Waaseyaa</a>&rsquo;s <code>oauth-provider</code> package wraps Google and GitHub OAuth 2.0 behind one <code>OAuthProviderInterface</code>. Until recently, both bundled providers only knew how to over-ask: Google always requested offline access with a forced consent prompt, and GitHub always made a second API call for the user&rsquo;s email, even when a consumer just needed a stable, verified identity to log someone in. The package now takes optional constructor arguments so an identity-only consumer can say exactly that, without touching the interface or forking the provider. What follows is the over-asking, the additive fix, and the two unsafe casts that got hardened along the way.</p>
<h2 id="what-over-asking-looked-like">What Over-Asking Looked Like</h2>
<p>Before this change:</p>
<ul>
<li><strong><code>GoogleOAuthProvider::getAuthorizationUrl()</code></strong> hardcoded <code>access_type=offline</code> and <code>prompt=consent</code> into every authorization URL. Every login requested refresh-token eligibility and forced a re-consent screen, whether or not the consumer ever stored a refresh token.</li>
<li><strong><code>GitHubOAuthProvider::getUserProfile()</code></strong> called <code>GET /user</code> and then unconditionally called <code>GET /user/emails</code>, even for a consumer that only reads the numeric <code>id</code> GitHub already returns from <code>GET /user</code>.</li>
<li><strong>Optional profile fields were unsafe.</strong> Google&rsquo;s <code>email</code>/<code>name</code> were read with a bare <code>(string) $data['email']</code> cast, and GitHub&rsquo;s <code>name</code> fallback cast <code>$userData['login']</code> the same way. A response missing either key triggered an &ldquo;Undefined array key&rdquo; warning; a malformed non-string value (an array, say) triggered an &ldquo;Array to string conversion&rdquo; warning and silently became the string <code>'Array'</code>. Waaseyaa&rsquo;s <code>phpunit.xml.dist</code> sets <code>failOnWarning=&quot;true&quot;</code>, so either defect turned into a hard test failure, not a quietly-ignored notice.</li>
</ul>
<h2 id="additive-configuration-not-a-parameter-bag">Additive Configuration, Not a Parameter Bag</h2>
<p>The fix rejected two easier options: a generic <code>array $options</code> bag on either provider (arbitrary, unvalidated URL rewriting) and a single <code>IdentityOnlyMode</code> toggle (which would have silently coupled scopes and refresh-token eligibility — concerns the interface already keeps separate). Instead, both providers gained optional trailing constructor parameters that default to the old behavior:</p>
<table>
	<thead>
			<tr>
					<th>Provider</th>
					<th>Default (unchanged)</th>
					<th>Identity-only</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Google</td>
					<td><code>accessType: Offline</code>, <code>forceConsent: true</code> → <code>access_type=offline&amp;prompt=consent</code></td>
					<td><code>accessType: Online</code>, <code>forceConsent: false</code> → <code>access_type=online</code>, <code>prompt</code> omitted entirely</td>
			</tr>
			<tr>
					<td>GitHub</td>
					<td><code>fetchEmail: true</code> → <code>GET /user</code> then <code>GET /user/emails</code></td>
					<td><code>fetchEmail: false</code> → only <code>GET /user</code>; profile has <code>email: ''</code>, <code>emailVerified: false</code></td>
			</tr>
	</tbody>
</table>
<p>A new <code>@api</code> enum backs the Google option instead of a raw string:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#a6e22e">enum</span> <span style="color:#a6e22e">GoogleAccessType</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Offline</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;offline&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">Online</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;online&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>GoogleOAuthProvider</code> builds the authorization URL from it, and only adds <code>prompt</code> when consent is forced:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$params <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;client_id&#39;</span>     <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">clientId</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;redirect_uri&#39;</span>  <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">redirectUri</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;response_type&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;code&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;scope&#39;</span>         <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">implode</span>(<span style="color:#e6db74">&#39; &#39;</span>, $scopes),
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;state&#39;</span>         <span style="color:#f92672">=&gt;</span> $state,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;access_type&#39;</span>   <span style="color:#f92672">=&gt;</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">accessType</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">value</span>,
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">forceConsent</span>) {
</span></span><span style="display:flex;"><span>    $params[<span style="color:#e6db74">&#39;prompt&#39;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;consent&#39;</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">AUTH_URL</span> <span style="color:#f92672">.</span> <span style="color:#e6db74">&#39;?&#39;</span> <span style="color:#f92672">.</span> <span style="color:#a6e22e">http_build_query</span>($params);
</span></span></code></pre></div><p>Per <a href="https://developers.google.com/identity/protocols/oauth2/web-server">Google&rsquo;s docs</a>, online is already the default when <code>access_type</code> is absent — so omitting <code>prompt</code> lets Google decide whether re-consent is needed instead of the framework forcing it. <code>forceConsent</code> is independent of <code>accessType</code>: turning off the prompt doesn&rsquo;t silently flip an offline consumer to online access.</p>
<p><code>GitHubOAuthProvider</code> skips the secondary lookup entirely rather than making the call and discarding the result:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$email <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>$emailVerified <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ($this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">fetchEmail</span>) {
</span></span><span style="display:flex;"><span>    $emailsResponse <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">httpClient</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">EMAILS_URL</span>, $headers);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($emailsResponse<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">isSuccess</span>()) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">foreach</span> ($emailsResponse<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">json</span>() <span style="color:#66d9ef">as</span> $entry) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// ...find the primary, verified email
</span></span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The existing fail-loud checks are untouched on both providers: a non-2xx response, a missing or empty <code>id</code>, both still block. An identity-only configuration still refuses a failed or unidentifiable response. It just stops paying for a lookup it would discard.</p>
<h2 id="using-it">Using It</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Waaseyaa\OAuthProvider\Provider\GoogleAccessType</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Waaseyaa\OAuthProvider\Provider\GoogleOAuthProvider</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> <span style="color:#a6e22e">Waaseyaa\OAuthProvider\Provider\GitHubOAuthProvider</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Google: online access, no forced re-consent prompt.
</span></span></span><span style="display:flex;"><span>$google <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GoogleOAuthProvider</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">clientId</span><span style="color:#f92672">:</span> $clientId,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">clientSecret</span><span style="color:#f92672">:</span> $clientSecret,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">redirectUri</span><span style="color:#f92672">:</span> $redirectUri,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">httpClient</span><span style="color:#f92672">:</span> $httpClient,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">accessType</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">GoogleAccessType</span><span style="color:#f92672">::</span><span style="color:#a6e22e">Online</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">forceConsent</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// GitHub: skip the secondary /user/emails lookup.
</span></span></span><span style="display:flex;"><span>$github <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">GitHubOAuthProvider</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">clientId</span><span style="color:#f92672">:</span> $clientId,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">clientSecret</span><span style="color:#f92672">:</span> $clientSecret,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">redirectUri</span><span style="color:#f92672">:</span> $redirectUri,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">httpClient</span><span style="color:#f92672">:</span> $httpClient,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fetchEmail</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>The existing four-argument constructor call on either class still compiles. It still produces byte-identical behavior — offline access with a forced prompt for Google, an email lookup for GitHub. An offline consumer that stores a refresh token, or one that needs a verified GitHub email, just keeps using the defaults.</p>
<h2 id="hardening-the-optional-fields">Hardening the Optional Fields</h2>
<p>The unsafe casts got fixed alongside the new parameters, since identity-only responses are exactly where a missing <code>email</code> or <code>name</code> shows up in practice. Google&rsquo;s profile parsing now guards both fields:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$email <span style="color:#f92672">=</span> <span style="color:#a6e22e">isset</span>($data[<span style="color:#e6db74">&#39;email&#39;</span>]) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">is_string</span>($data[<span style="color:#e6db74">&#39;email&#39;</span>]) <span style="color:#f92672">?</span> $data[<span style="color:#e6db74">&#39;email&#39;</span>] <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>$name <span style="color:#f92672">=</span> <span style="color:#a6e22e">isset</span>($data[<span style="color:#e6db74">&#39;name&#39;</span>]) <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">is_string</span>($data[<span style="color:#e6db74">&#39;name&#39;</span>]) <span style="color:#f92672">?</span> $data[<span style="color:#e6db74">&#39;name&#39;</span>] <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span></code></pre></div><p>GitHub&rsquo;s <code>name</code> fallback got the same treatment — falling back to <code>''</code> instead of casting a missing or non-string <code>login</code>. The required identity check is unchanged on both providers: a non-2xx response or an absent <code>id</code> still throws before any optional-field logic runs. Only the optional path stopped assuming the upstream payload is well-formed.</p>
<h2 id="why-this-matters-beyond-waaseyaa">Why This Matters Beyond Waaseyaa</h2>
<ul>
<li><strong>Prefer optional, default-preserving parameters over a config bag.</strong> A typed enum plus a couple of booleans is auditable at a glance; a generic <code>array $options</code> invites arbitrary, unvalidated behavior that&rsquo;s hard to review.</li>
<li><strong>Don&rsquo;t make a call you&rsquo;ll discard the result of.</strong> If a consumer configures identity-only, skip the secondary request outright rather than fetching and ignoring it — it&rsquo;s fewer round trips and one less thing that can rate-limit you.</li>
<li><strong>Optional response fields need <code>isset()</code> and a type check, not a bare cast.</strong> Any code path that only exercises the full/happy response will pass tests right up until a real provider omits a field you assumed was always present.</li>
</ul>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>