{"record":{"id":"83f0d81adb4d02f2","repo":"instructure/canvas-lms","slug":"invalid-grant","errorCode":"invalid_grant","errorMessage":"invalid_grant","messagePattern":"invalid_grant","errorType":"error_code","errorClass":"Canvas::OAuth::RequestError","httpStatus":null,"severity":"error","filePath":"lib/canvas/oauth/grant_types/authorization_code_with_pkce.rb","lineNumber":32,"sourceCode":"# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\n# details.\n#\n# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n\nmodule Canvas::OAuth\n  module GrantTypes\n    class AuthorizationCodeWithPKCE < AuthorizationCode\n      # PKCE can be used by public or confidential clients as defined in RFC 6749.\n      def allow_public_client?\n        true\n      end\n\n      private\n\n      def validate_type\n        unless Canvas::OAuth::PKCE.valid_code_verifier?(code: opts[:code], code_verifier: opts[:code_verifier])\n          raise Canvas::OAuth::RequestError, :invalid_grant\n        end\n\n        super\n      end\n    end\n  end\nend\n","sourceCodeStart":14,"sourceCodeEnd":40,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/canvas/oauth/grant_types/authorization_code_with_pkce.rb#L14-L40","documentation":"Canvas::OAuth::RequestError :invalid_grant is raised in AuthorizationCodeWithPkce#validate_type when the PKCE code_verifier supplied to the token endpoint does not match the code_challenge stored with the authorization code. Canvas::OAuth::PKCE.valid_code_verifier? hashes the verifier and compares it against the recorded challenge; failure aborts the exchange before the parent authorization-code validation runs.","triggerScenarios":"POST to /login/oauth2/token with grant_type=authorization_code and a code_verifier that is absent, generated with the wrong method (plain vs S256), not the exact verifier string for the code_challenge sent during /login/oauth2/auth, or regenerated per-request instead of persisted from the authorize step.","commonSituations":"SPA/mobile clients losing the verifier between the redirect and token exchange (new session, state not persisted); using the code_challenge as the verifier; re-running the token request with a freshly generated verifier; libraries defaulting to plain method while Canvas expects S256.","solutions":["Persist the code_verifier generated for the authorize request (session/storage) and send that exact string in the token exchange.","Generate the code_challenge with SHA-256 (S256): BASE64URL(SHA256(verifier)), and send code_challenge_method=S256 at the authorize step.","Send the plain verifier string, not the challenge, in the code_verifier parameter.","Retry the whole flow: start a new authorize request with a new verifier/challenge pair if the original pairing is lost."],"exampleFix":"// before: regenerating a verifier at exchange time\nconst codeVerifier = generateVerifier() // new random value -> invalid_grant\n\n// after: reuse the verifier stored at the authorize step\nconst codeVerifier = sessionStorage.getItem('pkce_verifier') // saved when redirecting to /login/oauth2/auth\nawait fetch(TOKEN_URL, { body: { grant_type: 'authorization_code', code, code_verifier: codeVerifier } })","handlingStrategy":"validation","validationCode":"const crypto = require('crypto')\nfunction validPkcePair(verifier, challenge) {\n  return typeof verifier === 'string' && verifier.length >= 43 && verifier.length <= 128 &&\n    challenge === crypto.createHash('sha256').update(verifier).digest('base64url')\n}","typeGuard":"function hasPkceVerifier(state) { return typeof state?.pkceVerifier === 'string' && state.pkceVerifier.length >= 43 }","tryCatchPattern":"try {\n  token = await exchangeCodeWithPkce(code, storedVerifier)\n} catch (e) {\n  if (e.body?.error === 'invalid_grant') {\n    // verifier/challenge mismatch: start a fresh PKCE flow\n    return startPkceFlow()\n  }\n  throw e\n}","preventionTips":["Persist the code_verifier (session or encrypted storage) at the authorize step and reuse it verbatim.","Always use code_challenge_method=S256 with BASE64URL(SHA256(verifier)).","Send the verifier, never the challenge, as code_verifier.","Generate a new verifier/challenge pair for every authorization attempt."],"tags":["oauth","pkce","code-verifier","token-exchange","canvas-lms"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}