Support OAuth 2.0 authorization_code login flow for Canvas CLI
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3582921. -->
Reported by: [mglaman](https://www.drupal.org/user/2416470)
Related to !865
>>>
<h3 id="overview">Overview</h3>
<p>The Canvas CLI currently authenticates using the OAuth 2.0 <code>client_credentials</code> flow. Credentials are configured via environment variables (<code>CANVAS_CLIENT_ID</code>, <code>CANVAS_CLIENT_SECRET</code>) in a project-local <code>.env</code> file or the global <code>~/.canvasrc</code> file. This requires developers to manually create a Consumer in Drupal admin and copy its credentials — authentication is not tied to a specific Drupal user.</p>
<p>To enable user-specific authentication, Canvas should also support an interactive OAuth 2.0 <code>authorization_code</code> flow with PKCE. This lets developers authenticate as a real Drupal user via their browser, so CLI operations are subject to that user's roles and permissions — matching how the Canvas UI behaves.</p>
<p>The CLI should:</p>
<ul>
<li>Discover the authorization server dynamically from the target site.</li>
<li>
Prefer discovery via <code>/.well-known/oauth-protected-resource</code> (RFC 9728),<br>
which provides the authorization server metadata URL.
</li>
<li>
If <code>/.well-known/oauth-protected-resource</code> returns a 404, fall back to<br>
Simple OAuth's well-known endpoints on the site (<code>/oauth/authorize</code> and<br>
<code>/oauth/token</code>), which are always present when <code>canvas_oauth</code><br>
is enabled.
</li>
<li>
Read the client ID from the <code>X-Consumer-ID</code> header returned by the Canvas API during discovery, so developers do not need to manually configure <code>CANVAS_CLIENT_ID</code>.
</li>
<li>
Guide the user through an interactive browser-based login using the OAuth 2.0 <code>authorization_code</code> flow with PKCE.
</li>
<li>
Persist the resulting access and refresh tokens to <code>~/.canvasrc</code> so subsequent commands (<code>canvas push</code>, <code>canvas pull</code>, etc.) work without needing client credentials.
</li>
</ul>
<h3 id="proposed-resolution">Proposed resolution</h3>
<ol>
<li>
<strong>CLI login command using authorization_code + PKCE</strong>
<ul>
<li>
Add a <code>canvas auth:login</code> command that:
<ul>
<li>
Accepts <code>--site-url</code> (or reads <code>CANVAS_SITE_URL</code>).
</li>
<li>
Attempts to discover the authorization server from <code>/.well-known/oauth-protected-resource</code>. If that returns 404, falls back to using Simple OAuth's endpoints directly (<code>/oauth/authorize</code> and <code>/oauth/token</code>).
</li>
<li>
Reads the <code>X-Consumer-ID</code> header from the Canvas API to determine the client ID automatically.
</li>
<li>
Initiates an OAuth 2.0 <code>authorization_code</code> flow with PKCE:
<ul>
<li>Generates the PKCE code verifier and code challenge.</li>
<li>Starts a temporary local HTTP server to receive the redirect callback.</li>
<li>Opens the user's browser to log in and authorize the CLI.</li>
<li>
Receives the authorization code via the local redirect and exchanges it for tokens at the token endpoint using PKCE.
</li>
</ul>
</li>
<li>
Persists the access token, refresh token, and associated metadata (site URL, client ID, token expiry) to <code>~/.canvasrc</code>.
</li>
<li>
After login, subsequent commands (<code>canvas push</code>, <code>canvas pull</code>, etc.) should use the stored user token automatically, without requiring <code>CANVAS_CLIENT_ID</code> or <code>CANVAS_CLIENT_SECRET</code>.
</li>
</ul>
</li>
<li>
Provide clear CLI feedback, including:
<ul>
<li>Whether login succeeded or failed.</li>
<li>Which Drupal user account is now authenticated.</li>
<li>
Actionable error messages for common problems (authorization denied, network errors, misconfigured endpoints, invalid or expired codes).
</li>
</ul>
</li>
</ul>
</li>
<li>
<strong>Token storage and refresh</strong>
<ul>
<li>
Store user auth tokens in <code>~/.canvasrc</code>, keyed by site URL so multiple sites are supported.
</li>
<li>
The existing token-refresh logic in <code>ApiService</code> currently only handles <code>client_credentials</code> re-authentication. Extend it to use the stored refresh token when a user-auth token expires, rather than falling back to client credentials.
</li>
<li>
If the refresh token is also expired (or absent), prompt the user to run <code>canvas auth:login</code> again.
</li>
</ul>
</li>
<li>
<strong>Configuration and discovery</strong>
<ul>
<li>
Document and implement the discovery order:
<ol>
<li>
Fetch <code>/.well-known/oauth-protected-resource</code>; if successful, use the <code>authorization_servers</code> field to locate the authorization server metadata (<code>/.well-known/oauth-authorization-server</code> or <code>/.well-known/openid-configuration</code>), which provides both <code>authorization_endpoint</code> and <code>token_endpoint</code>.
</li>
<li>
If the above returns 404, fall back to Simple OAuth's conventional endpoints: <code>/oauth/authorize</code> and <code>/oauth/token</code>.
</li>
</ol>
</li>
<li>
Read the OAuth client ID from the <code>X-Consumer-ID</code> response header on the Canvas API, which is supplied by the Consumers module. Document this as the zero-config path; allow <code>--client-id</code> as an override for non-standard setups.
</li>
<li>
The redirect URI used for PKCE should be <code>http://localhost:PORT/callback</code> on a dynamically selected port (or a fixed well-known port). Document that the Consumer in Drupal admin must have this redirect URI registered.
</li>
</ul>
</li>
<li>
<strong>Optional logout command</strong>
<ul>
<li>
Add a <code>canvas auth:logout</code> command that:
<ul>
<li>Removes the stored user tokens from <code>~/.canvasrc</code>.</li>
<li>
When the authorization server supports RFC 7009 token revocation, calls the revocation endpoint to invalidate the tokens server-side.
</li>
</ul>
</li>
</ul>
</li>
</ol>
issue