instructure/canvas-lms · error · Canvas::OAuth::RequestError
invalid_grant
invalid_grant
Error message
invalid_grant
What it means
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.
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.
Example fix
// before: regenerating a verifier at exchange time
const codeVerifier = generateVerifier() // new random value -> invalid_grant
// after: reuse the verifier stored at the authorize step
const codeVerifier = sessionStorage.getItem('pkce_verifier') // saved when redirecting to /login/oauth2/auth
await fetch(TOKEN_URL, { body: { grant_type: 'authorization_code', code, code_verifier: codeVerifier } }) Defensive patterns
Strategy: validation
Validate before calling
const crypto = require('crypto')
function validPkcePair(verifier, challenge) {
return typeof verifier === 'string' && verifier.length >= 43 && verifier.length <= 128 &&
challenge === crypto.createHash('sha256').update(verifier).digest('base64url')
} Type guard
function hasPkceVerifier(state) { return typeof state?.pkceVerifier === 'string' && state.pkceVerifier.length >= 43 } Try / catch
try {
token = await exchangeCodeWithPkce(code, storedVerifier)
} catch (e) {
if (e.body?.error === 'invalid_grant') {
// verifier/challenge mismatch: start a fresh PKCE flow
return startPkceFlow()
}
throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- incorrect_client
- invalid_authorization_code
- invalid_client_id
- invalid_client_secret
- unsupported_grant_type
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/83f0d81adb4d02f2.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas/oauth/grant_types/authorization_code_with_pkce.rb:32
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Canvas::OAuth
module GrantTypes
class AuthorizationCodeWithPKCE < AuthorizationCode
# PKCE can be used by public or confidential clients as defined in RFC 6749.
def allow_public_client?
true
end
private
def validate_type
unless Canvas::OAuth::PKCE.valid_code_verifier?(code: opts[:code], code_verifier: opts[:code_verifier])
raise Canvas::OAuth::RequestError, :invalid_grant
end
super
end
end
end
end
View on GitHub (pinned to 1c9f0bb801)