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:
parent
a0009a9991
commit
0b0e7d53a1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue