Add API to get LTI 2 tokens for SiteAdmins

refs INTEROP-7211
flag=none

Test plan
- Set up an LTI 2 tool (lti_originality_report
  _example works)
- Go to api/lti/lti_2_token with either
  basic_launch_lti2_id (from an assignment's
  Plagiarism URL launch there's an ID
  in there--it's a message handler ID)
  OR the tool_proxy_id of a Tool Proxy
- It should return a JWT
- Use the JWT with an LTI 2 API like
  /api/lti/users/:user_id and make
  sure the auth works

Change-Id: I385572d9c6eba2fa062505bcc705da27ec68488c
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/289457
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Xander Moffatt <xmoffatt@instructure.com>
QA-Review: Evan Battaglia <ebattaglia@instructure.com>
Product-Review: Mysti Lilla <mysti@instructure.com>
This commit is contained in:
Mysti Lilla 2022-04-11 20:24:26 -06:00
parent a0009a9991
commit 0b0e7d53a1
4 changed files with 220 additions and 141 deletions

View File

@ -23,7 +23,7 @@ class Lti::TokenController < ApplicationController
include SupportHelpers::ControllerHelpers
before_action :require_site_admin
before_action :verify_1_3_tool
before_action :verify_1_3_tool, except: :lti_2_token
# site-admin-only action to get an LTI 1.3 Access Token for any ContextExternalTool
# specified by `tool_id`, or for any DeveloperKey specified by `client_id`.
@ -41,6 +41,17 @@ class Lti::TokenController < ApplicationController
render json: provider.generate_token
end
def lti_2_token
unless tool_proxy
return render json: {
status: :bad_request,
errors: [{ message: "Unable to find tool for given parameters" }]
}, status: :bad_request
end
token = Lti::OAuth2::AccessToken.create_jwt(aud: request.host, sub: tool_proxy.guid)
render json: token.to_s
end
private
def verify_1_3_tool
@ -59,4 +70,12 @@ class Lti::TokenController < ApplicationController
ContextExternalTool.find(params.require(:tool_id)).developer_key
end
end
def tool_proxy
@tool_proxy ||= if params[:basic_launch_lti2_id]
Lti::MessageHandler.find(params.require(:basic_launch_lti2_id)).tool_proxy
else
Lti::ToolProxy.find params.require(:tool_proxy_id)
end
end
end

View File

@ -2618,6 +2618,7 @@ CanvasRails::Application.routes.draw do
end
# LTI Access Tokens (Site Admin only)
get "lti_2_token", controller: "lti/token", action: :lti_2_token, as: :lti_2_token_site_admin
get "advantage_token", controller: "lti/token", action: :advantage_access_token, as: :lti_advantage_token_site_admin
end

View File

@ -14,7 +14,7 @@ There are two components to local development:
### Kinesis Stream
To enabled Live Events, you need to configure the plugin in the /plugins
To enable Live Events, you need to configure the plugin in the /plugins
interface. If using the docker-compose dev setup, there is a "fake
kinesis" available in docker-compose/kinesis.override.yml available for
use. Once it's up, make sure you have the `aws` cli installed, and run

View File

@ -16,162 +16,221 @@
#
# 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/>.
require "lti2_spec_helper"
describe Lti::TokenController do
let_once(:developer_key) do
key = DeveloperKey.create!(
account: root_account,
is_lti_key: true,
public_jwk_url: "http://test.host/jwks"
)
enable_developer_key_account_binding!(key)
key
end
let_once(:tool) do
ContextExternalTool.create!(
context: root_account,
consumer_key: "key",
shared_secret: "secret",
name: "test tool",
url: "http://www.tool.com/launch",
developer_key: developer_key,
settings: { use_1_3: true },
workflow_state: "public"
)
end
let(:root_account) { Account.create!(name: "root account") }
let(:parsed_body) { JSON.parse(response.body) }
let(:decoded_jwt) { JSON::JWT.decode parsed_body["access_token"], :skip_verification }
let(:params) { {} }
def send_request
get :advantage_access_token, params: params, as: :json
end
context "when user is not logged in" do
it "returns unauthorized" do
send_request
expect(response).to be_unauthorized
describe "#advantage_access_token" do
let_once(:developer_key) do
key = DeveloperKey.create!(
account: root_account,
is_lti_key: true,
public_jwk_url: "http://test.host/jwks"
)
enable_developer_key_account_binding!(key)
key
end
end
let_once(:tool) do
ContextExternalTool.create!(
context: root_account,
consumer_key: "key",
shared_secret: "secret",
name: "test tool",
url: "http://www.tool.com/launch",
developer_key: developer_key,
settings: { use_1_3: true },
workflow_state: "public"
)
end
let(:root_account) { Account.create!(name: "root account") }
let(:parsed_body) { JSON.parse(response.body) }
let(:decoded_jwt) { JSON::JWT.decode parsed_body["access_token"], :skip_verification }
let(:params) { {} }
context "when user is not site admin" do
before do
user_session(account_admin_user(account: root_account))
def send_request
get :advantage_access_token, params: params, as: :json
end
it "returns unauthorized" do
send_request
expect(response).to be_unauthorized
end
end
context "when user is site admin" do
let(:user) { site_admin_user }
before do
user_session(user)
end
shared_examples_for "a normal LTI access token" do
it "uses all LTI scopes" do
context "when user is not logged in" do
it "returns unauthorized" do
send_request
expect(decoded_jwt[:scopes]).to eq TokenScopes::LTI_SCOPES.keys.join(" ")
expect(parsed_body["scope"]).to eq TokenScopes::LTI_SCOPES.keys.join(" ")
end
it "uses request host for aud claim" do
send_request
expect(decoded_jwt[:aud]).to eq "http://test.host/login/oauth2/token"
end
it "returns 200" do
send_request
expect(response).to be_successful
end
it "includes user id in custom claim for tracking purposes" do
send_request
expect(decoded_jwt["canvas.instructure.com"]["token_generated_by"]).to eq user.global_id
end
it "includes site admin custom claim for tracking purposes" do
send_request
expect(decoded_jwt["canvas.instructure.com"]["token_generated_for"]).to eq "site_admin"
expect(response).to be_unauthorized
end
end
context "when client_id is provided" do
let(:params) { { client_id: developer_key.global_id } }
it "uses client_id as sub claim" do
send_request
expect(decoded_jwt[:sub]).to eq developer_key.global_id
context "when user is not site admin" do
before do
user_session(account_admin_user(account: root_account))
end
it_behaves_like "a normal LTI access token"
it "returns unauthorized" do
send_request
expect(response).to be_unauthorized
end
end
context "when user is site admin" do
let(:user) { site_admin_user }
before do
user_session(user)
end
shared_examples_for "a normal LTI access token" do
it "uses all LTI scopes" do
send_request
expect(decoded_jwt[:scopes]).to eq TokenScopes::LTI_SCOPES.keys.join(" ")
expect(parsed_body["scope"]).to eq TokenScopes::LTI_SCOPES.keys.join(" ")
end
it "uses request host for aud claim" do
send_request
expect(decoded_jwt[:aud]).to eq "http://test.host/login/oauth2/token"
end
it "returns 200" do
send_request
expect(response).to be_successful
end
it "includes user id in custom claim for tracking purposes" do
send_request
expect(decoded_jwt["canvas.instructure.com"]["token_generated_by"]).to eq user.global_id
end
it "includes site admin custom claim for tracking purposes" do
send_request
expect(decoded_jwt["canvas.instructure.com"]["token_generated_for"]).to eq "site_admin"
end
end
context "when client_id is provided" do
let(:params) { { client_id: developer_key.global_id } }
it "uses client_id as sub claim" do
send_request
expect(decoded_jwt[:sub]).to eq developer_key.global_id
end
it_behaves_like "a normal LTI access token"
end
context "when tool_id is provided" do
let(:params) { { tool_id: tool.global_id } }
it "uses tool's developer key id as sub claim" do
send_request
expect(decoded_jwt[:sub]).to eq developer_key.global_id
end
it_behaves_like "a normal LTI access token"
end
context "when non-LTI key is provided" do
let(:other_key) do
key = DeveloperKey.create!(account: root_account)
enable_developer_key_account_binding!(key)
key
end
let(:params) { { client_id: other_key.global_id } }
it "returns 400" do
send_request
expect(response).to be_bad_request
end
end
context "when non-LTI-1.3 tool is provided" do
let(:other_key) do
key = DeveloperKey.create!(account: root_account)
enable_developer_key_account_binding!(key)
key
end
let(:other_tool) do
ContextExternalTool.create!(
context: root_account,
consumer_key: "key",
shared_secret: "secret",
name: "test tool",
url: "http://www.tool.com/launch",
developer_key: other_key,
settings: { use_1_3: false },
workflow_state: "public"
)
end
let(:params) { { tool_id: other_tool.global_id } }
it "returns 400" do
send_request
expect(response).to be_bad_request
end
end
end
end
describe "#lti_2_token" do
include_context "lti2_spec_helper"
let(:params) { {} }
def send_request
get :lti_2_token, params: params, as: :json
end
context "when user is not logged in" do
it "returns unauthorized" do
send_request
expect(response).to be_unauthorized
end
end
context "when user is not site admin" do
before do
user_session(account_admin_user)
end
it "returns unauthorized" do
send_request
expect(response).to be_unauthorized
end
end
shared_examples_for "creates an LTI 2 token" do
let(:user) { site_admin_user }
before do
user_session(user)
end
it "creates a token" do
send_request
decoded_jwt = Canvas::Security.decode_jwt(response.body).with_indifferent_access
expect(decoded_jwt[:sub]).to eq tool_proxy.guid
end
end
context "when basic_launch_lti2_id is provided" do
let(:params) { { basic_launch_lti2_id: message_handler.global_id } }
it_behaves_like "creates an LTI 2 token"
end
context "when tool_id is provided" do
let(:params) { { tool_id: tool.global_id } }
let(:params) { { tool_proxy_id: tool_proxy.global_id } }
it "uses tool's developer key id as sub claim" do
send_request
expect(decoded_jwt[:sub]).to eq developer_key.global_id
end
it_behaves_like "a normal LTI access token"
end
context "when non-LTI key is provided" do
let(:other_key) do
key = DeveloperKey.create!(account: root_account)
enable_developer_key_account_binding!(key)
key
end
let(:params) { { client_id: other_key.global_id } }
it "returns 400" do
send_request
expect(response).to be_bad_request
end
end
context "when non-LTI-1.3 tool is provided" do
let(:other_key) do
key = DeveloperKey.create!(account: root_account)
enable_developer_key_account_binding!(key)
key
end
let(:other_tool) do
ContextExternalTool.create!(
context: root_account,
consumer_key: "key",
shared_secret: "secret",
name: "test tool",
url: "http://www.tool.com/launch",
developer_key: other_key,
settings: { use_1_3: false },
workflow_state: "public"
)
end
let(:params) { { tool_id: other_tool.global_id } }
it "returns 400" do
send_request
expect(response).to be_bad_request
end
it_behaves_like "creates an LTI 2 token"
end
end
end