instructure / canvas-lms

The open LMS by Instructure, Inc.
https://github.com/instructure/canvas-lms/wiki
GNU Affero General Public License v3.0
5.61k stars 2.48k forks source link

/login/oauth2/token endpoint throwing error "JWS signature invalid" #1872

Closed ojhaashish13 closed 3 years ago

ojhaashish13 commented 3 years ago

Summary:

Oauth2 endpoint post token service "/login/oauth2/token" throwing 400 status response with "JWS signature invalid" error. I have used correct endpoint, parameters and used post method to call the URL https:///login/oauth2/token. My JWT data is valid as per jwt.io debugger when used public key to validate signature.

Steps to reproduce:

I am working on LTI 1.3 integration + Assignment grade services.

  1. I have successfully installed the canvas instance by following this link https://github.com/instructure/canvas-lms/wiki/Production-Start 
  2. My LTI 1.3 application launched working fine.
  3. But while using the Assignment Grade Services to post score, I am not getting access token instead I am getting the "JWS signature invalid" with 400 header status.
  4. I have followed this link to make a post request to get the access token. token https://canvas.instructure.com/doc/api/file.oauth_endpoints.html#post-login-oauth2-token 
  5. Here is the information contained in the body of the post request:
{
  "grant_type": "client_credentials",
  "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
  "client_assertion": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjNPaVdic1RwQzRlZEZvSkdqbGlGemdGUVFmZ0JIblBrTGJ1RWpYS2tiaG1VbllqRHNjYWdsaEllRU1RZUpWQnYifQ.eyJpc3MiOiJodHRwczovL3N0YWdpbmcubXlkb21haW4uY29tIiwic3ViIjoiMTAwMDAwMDAwMDAwMDEiLCJhdWQiOiJodHRwczovL2NhbnZhcy5teWRvbWFpbi5jb20vbG9naW4vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNjIwNzk2OTQ4LCJleHAiOjE2MjA3OTcwMTMsImp0aSI6Imx0aS1zZXJ2aWNlLXRva2VuYmRmMzlkNTFmMDYxNmZhMWMzMGJkYTJmYTQ2YWU3NmFmZjI3YjFlMTU2YWZiNmVmYjU2MTBlMDU3ZWUwMWM3ZiJ9.bSELJ3HQSQJQpeh75x_OOfBAZIWrMprYD9bK7S6nA1NUi483Yxgk2MDT6ZYRP9-eI03HoQdr9aP2Wv121qm9SuMO0KLUZgQ01rO3GmfyZhOS9yat0AVJvOxiEyRNSJD9W4M4ZcHBPw8UJ_KnywGCKRKRiBBih2JLoquwmP9NomHNvKlW--9pUM3amGH1z9P940kJIqdcVulSTDw2VUBnsjd1J6ngkG4DKgi1BwDaldhhU0yPzkiAO-UJRJHoqrxkYnYfJ5Q_Y6SnvgsHKAMiRNnhHKcqQetWh_4mTYl0Qmh3h6B6EafjVdxIdcMiIyCJGz3cUn8_XigCu1qPXBvEwA",
  "scope": "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly https://purl.imsglobal.org/spec/lti-ags/scope/score"
}

Decoded JWT: Header:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "3OiWbsTpC4edFoJGjliFzgFQQfgBHnPkLbuEjXKkbhmUnYjDscaglhIeEMQeJVBv"
}

Payload:

{
  "iss": "https://staging.mydomain.com",
  "sub": "10000000000001",
  "aud": "https://canvas.mydomain.com/login/oauth2/token",
  "iat": 1620796948,
  "exp": 1620797013,
  "jti": "lti-service-tokenbdf39d51f0616fa1c30bda2fa46ae76aff27b1e156afb6efb5610e057ee01c7f"
}

I can verify the JWT data place inside the "client_assertion" field using my public key and as per JWT.io debugger signature is valid.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6ljLtgYaAP3ByNWEt5WD
aL8BpOc+BvVljjigdLipFI5Mk1Ey9XdM7ywIL5r5zDMRLokGh3p/TinjufVd48D7
It0gtOQ8gLTQ+5Qsi+wcm2mSKRj/nz20S1bdbsozbg5JrIN2goo/o9DXgoe9+FL0
PenWrDjOYQP8cLc5b0LcYDP6t81NdmJBG63toOXUXhOTip3u7GjnK+Zi+9TSZm2T
v+U27aRY4ihJ0vqx7k2HvEIOPxZuVHLn8LGMUwErSPNvm7t/t0WkYtnPR+/yN/bA
4DRNR0dWPlns3KLyDAVpN+bZYCZDbHFI0zj6Lh+YhNSRA7KrY9C5zoPvd3QP5jq7
JQIDAQAB
-----END PUBLIC KEY-----
  1. Inside the canvas platform,  the LTI 1.3 tool configuration contains mentioned Public JWK data and I have enabled all permissions under LTI Advantage Services. { "kty": "RSA", "alg": "RS256", "use": "sig", "e": "AQAB", "n": "6ljLtgYaAP3ByNWEt5WDaL8BpOc-BvVljjigdLipFI5Mk1Ey9XdM7ywIL5r5zDMRLokGh3p_TinjufVd48D7It0gtOQ8gLTQ-5Qsi-wcm2mSKRj_nz20S1bdbsozbg5JrIN2goo_o9DXgoe9-FL0PenWrDjOYQP8cLc5b0LcYDP6t81NdmJBG63toOXUXhOTip3u7GjnK-Zi-9TSZm2Tv-U27aRY4ihJ0vqx7k2HvEIOPxZuVHLn8LGMUwErSPNvm7t_t0WkYtnPR-_yN_bA4DRNR0dWPlns3KLyDAVpN-bZYCZDbHFI0zj6Lh-YhNSRA7KrY9C5zoPvd3QP5jq7JQ", "kid": "3OiWbsTpC4edFoJGjliFzgFQQfgBHnPkLbuEjXKkbhmUnYjDscaglhIeEMQeJVBv" } My public key is correct and has used the same kid while configuring the tool inside the platform.

I am totally stuck and no idea how to fix this or debug it further, please suggest a solution.

Expected behavior:

It should generate the access token.

Actual behavior:

Throwing 400 header status with mentioned error {"error":"invalid_request","error_description":"JWS signature invalid."}

Additional notes:

westonkd commented 3 years ago

Hello @ojhaashish13, I'd be happy to help troubleshoot the issue you are seeing!

The "JWS signature invalid" error response generally indicates one of three things. You've already done some good debugging, so we can probably rule some of these out:

  1. The Canvas developer key identified by the sub in the client_assertion does not exist
  2. The Canvas developer key identified by the sub in the client_assertion does not have an associated public key
  3. The signature of the client_assertion was invalid in some way or used an unexpected algorithm.

Just to be very sure, I'll run through verifying each of these potential issues.

All of these steps will require accessing the Canvas rails console. If you are running Canvas without Docker, this can be done with bundle exec rails c in the canvas-lms repository.If you are using Docker with your Canvas setup, this can be done with docker-compose run --rm web rails c

1. Verify the key exists

I'm quite sure you've already done this from the UI, but let's double-check.

Judging by the client_assertion you provided as your example, your Developer Key ID is 10000000000001

From the Canvas Rails console, run the following:

key = DeveloperKey.find(10000000000001)
ap key

If no exception is thrown, we can conclude that the developer key exists.

2. Verify the key has an associated public key

It also sounds like you've confirmed this, but let's double-check

From the same Rails console, run the following:

key = DeveloperKey.find(10000000000001)

# One of the following two lines should print your public key or public key URL
key.public_jwk
key.public_jwk_url

If one of the last lines printed your public key in JWK format (or URL pointing to it) we are good to go here.

3. Verify the the client_assertion signature validation Canvas is working as you expect:

From the same Rails console, run the following:

key = DeveloperKey.find(10000000000001)

# I'm just using the client assertion from your example, but please update with a newly created assertion so
# the iat and exp claims are within valid ranges.
client_assertion = ''eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjNPaVdic1RwQzRlZEZvSkdqbGlGemdGUVFmZ0JIblBrTGJ1RWpYS2tiaG1VbllqRHNjYWdsaEllRU1RZUpWQnYifQ.eyJpc3MiOiJodHRwczovL3N0YWdpbmcubXlkb21haW4uY29tIiwic3ViIjoiMTAwMDAwMDAwMDAwMDEiLCJhdWQiOiJodHRwczovL2NhbnZhcy5teWRvbWFpbi5jb20vbG9naW4vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNjIwNzk2OTQ4LCJleHAiOjE2MjA3OTcwMTMsImp0aSI6Imx0aS1zZXJ2aWNlLXRva2VuYmRmMzlkNTFmMDYxNmZhMWMzMGJkYTJmYTQ2YWU3NmFmZjI3YjFlMTU2YWZiNmVmYjU2MTBlMDU3ZWUwMWM3ZiJ9.bSELJ3HQSQJQpeh75x_OOfBAZIWrMprYD9bK7S6nA1NUi483Yxgk2MDT6ZYRP9-eI03HoQdr9aP2Wv121qm9SuMO0KLUZgQ01rO3GmfyZhOS9yat0AVJvOxiEyRNSJD9W4M4ZcHBPw8UJ_KnywGCKRKRiBBih2JLoquwmP9NomHNvKlW--9pUM3amGH1z9P940kJIqdcVulSTDw2VUBnsjd1J6ngkG4DKgi1BwDaldhhU0yPzkiAO-UJRJHoqrxkYnYfJ5Q_Y6SnvgsHKAMiRNnhHKcqQetWh_4mTYl0Qmh3h6B6EafjVdxIdcMiIyCJGz3cUn8_XigCu1qPXBvEwA"
key = DeveloperKey.find(10000000000001)

# Attempt to decode the JWT with signature verification:
# Note that this assumes your developer key has a public_jwk rather than a 
# public_jwk_url
JSON::JWT.decode(client_assertion, JSON::JWK.new(key.public_jwk), :RS256)

The above code should not raise any exceptions. If an exception is raised, the name of that exception should help point you towards the problem.

Other notes

One other thing I noticed was that the aud claim in the client_assertion may not be correct.

Currently, it is set to https://canvas.mydomain.com/login/oauth2/token. The aud claim should exactly match the URL you are making a request to when retrieving the access token.

Is https://canvas.mydomain.com where your local Canvas is hosted? If it is not, you should update the aud to match where your local Canvas is hosted (including port number).

Here is an example aud for my personal Canvas development environment (Docker):

{
  "iss": "ltitesttool.docker",
  "sub": "10000000000005",
  "aud": "http://canvas.docker/login/oauth2/token",
  "iat": 1620921299,
  "exp": 1620921899,
  "jti": "777a1ccd-58cb-4645-8db6-a8aab6b5e7e4"
}
ojhaashish13 commented 3 years ago

Thank you so much for the detailed debug instructions. I followed your suggestions and solution #2. "Verify the key has an associated public key" worked. In my case, both JWKS Url and JWKS JSON added to the tool configuration. I updated the tool configuration, kept only JWKS token; removed the JWKS Url and it worked.

westonkd commented 3 years ago

No problem! I'm glad you got it sorted out.