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 &mdash; 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 &mdash; 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