Add asymmetric encryption for service tokens

refs FOO-2410

test plan:
- in dynamic_settings.yml, add the following block:
```
store:
    canvas:
      services-jwt:
        # these are all the same JWK but with different kid
        # to generate a new key, run the following in a Canvas console:
        #
        # key = OpenSSL::PKey::RSA.generate(2048)
        # key.public_key.to_jwk(kid: Time.now.utc.iso8601).to_json
        jwk-past.json: "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ\",\"kid\":\"2018-05-18T22:33:20Z_a\",\"d\":\"pYwR64x-LYFtA13iHIIeEvfPTws50ZutyGfpHN-kIZz3k-xVpun2Hgu0hVKZMxcZJ9DkG8UZPqD-zTDbCmCyLQ\",\"p\":\"6OQ2bi_oY5fE9KfQOcxkmNhxDnIKObKb6TVYqOOz2JM\",\"q\":\"y-UBef95njOrqMAxJH1QPds3ltYWr8QgGgccmcATH1M\",\"dp\":\"Ol_xkL7rZgNFt_lURRiJYpJmDDPjgkDVuafIeFTS4Ic\",\"dq\":\"RtzDY5wXr5TzrwWEztLCpYzfyAuF_PZj1cfs976apsM\",\"qi\":\"XA5wnwIrwe5MwXpaBijZsGhKJoypZProt47aVCtWtPE\"}"
        jwk-present.json: "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ\",\"kid\":\"2018-06-18T22:33:20Z_b\",\"d\":\"pYwR64x-LYFtA13iHIIeEvfPTws50ZutyGfpHN-kIZz3k-xVpun2Hgu0hVKZMxcZJ9DkG8UZPqD-zTDbCmCyLQ\",\"p\":\"6OQ2bi_oY5fE9KfQOcxkmNhxDnIKObKb6TVYqOOz2JM\",\"q\":\"y-UBef95njOrqMAxJH1QPds3ltYWr8QgGgccmcATH1M\",\"dp\":\"Ol_xkL7rZgNFt_lURRiJYpJmDDPjgkDVuafIeFTS4Ic\",\"dq\":\"RtzDY5wXr5TzrwWEztLCpYzfyAuF_PZj1cfs976apsM\",\"qi\":\"XA5wnwIrwe5MwXpaBijZsGhKJoypZProt47aVCtWtPE\"}"
        jwk-future.json: "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"uX1MpfEMQCBUMcj0sBYI-iFaG5Nodp3C6OlN8uY60fa5zSBd83-iIL3n_qzZ8VCluuTLfB7rrV_tiX727XIEqQ\",\"kid\":\"2018-07-18T22:33:20Z_c\",\"d\":\"pYwR64x-LYFtA13iHIIeEvfPTws50ZutyGfpHN-kIZz3k-xVpun2Hgu0hVKZMxcZJ9DkG8UZPqD-zTDbCmCyLQ\",\"p\":\"6OQ2bi_oY5fE9KfQOcxkmNhxDnIKObKb6TVYqOOz2JM\",\"q\":\"y-UBef95njOrqMAxJH1QPds3ltYWr8QgGgccmcATH1M\",\"dp\":\"Ol_xkL7rZgNFt_lURRiJYpJmDDPjgkDVuafIeFTS4Ic\",\"dq\":\"RtzDY5wXr5TzrwWEztLCpYzfyAuF_PZj1cfs976apsM\",\"qi\":\"XA5wnwIrwe5MwXpaBijZsGhKJoypZProt47aVCtWtPE\"}"
```
- Ensure /internal/services/jwks loads correctly
- In console, ensure `CanvasSecurity::ServicesJwt.decrypt(Base64.decode64(CanvasSecurity::ServicesJwt.for_user('localhost', User.first)))`
and `CanvasSecurity::ServicesJwt.decrypt(Base64.decode64(CanvasSecurity::ServicesJwt.for_user('localhost', User.first, symmetric: true)))`
both work and produce sensible looking output

Change-Id: I13c6c35cc92ed12d03bf97e89e590614e11c6d47
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275160
QA-Review: August Thornton <august@instructure.com>
Product-Review: August Thornton <august@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Ethan Vizitei <evizitei@instructure.com>
Reviewed-by: Evan Battaglia <ebattaglia@instructure.com>
This commit is contained in:
Jacob Burroughs 2021-10-04 14:20:31 -05:00
parent 121d12055d
commit 214014049f
47 changed files with 635 additions and 552 deletions

View File

@ -53,8 +53,10 @@ class JwtsController < ApplicationController
#
# @returns JWT
def create
services_jwt = Canvas::Security::ServicesJwt
.for_user(request.host_with_port, @current_user, real_user: @real_current_user)
services_jwt = CanvasSecurity::ServicesJwt
.for_user(request.host_with_port, @current_user, real_user: @real_current_user,
# TODO: remove this once we teach all consumers to consume the asymmetric ones
symmetric: true)
render json: { token: services_jwt }
end
@ -84,14 +86,16 @@ class JwtsController < ApplicationController
status: 400
)
end
services_jwt = Canvas::Security::ServicesJwt.refresh_for_user(
services_jwt = CanvasSecurity::ServicesJwt.refresh_for_user(
params[:jwt],
request.host_with_port,
@current_user,
real_user: @real_current_user
real_user: @real_current_user,
# TODO: remove this once we teach all consumers to consume the asymmetric ones
symmetric: true
)
render json: { token: services_jwt }
rescue Canvas::Security::ServicesJwt::InvalidRefresh
rescue CanvasSecurity::ServicesJwt::InvalidRefresh
render(
json: { errors: { jwt: "invalid refresh" } },
status: 400

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
#
# Copyright (C) 2018 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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 Lti::Ims
# @API Security
# @internal
#
# TODO: remove internal flags
#
# Security api for IMS LTI 1.3.
#
# @model JWKs
# {
# "id": "JWKs",
# "description": "",
# "properties": {
# "keys": {
# "description": "The set of JWK objects avaiable to verify JWS signature."
# "type": "array",
# "items": {
# "kty": "string"
# "kid": "string",
# "alg": "string"
# },
# "example": ["{\"kty\":\"RSA\",\"n\":\"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx\\n 4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs\\n tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2\\n QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI\\n SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb\\n w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\",\"e\":\"AQAB\",\"alg\":\"RS256\",\"kid\":\"2011-04-29\"}"]
# }
# }
# }
#
class SecurityController < ApplicationController
skip_before_action :load_user
# @API Show all available JWKs used by Canvas for signing.
#
# @returns JWKs
def jwks
keys = Lti::KeyStorage.public_keyset
response.set_header('Cache-Control', "max-age=#{Lti::KeyStorage.max_cache_age}")
render json: { keys: keys }
end
end
end

View File

@ -170,12 +170,6 @@ class Oauth2ProviderController < ApplicationController
render json: response
end
def jwks
keys = Canvas::Oauth::KeyStorage.public_keyset
response.set_header('Cache-Control', "max-age=#{Canvas::Oauth::KeyStorage.max_cache_age}")
render json: { keys: keys }
end
private
def oauth_error(exception)

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
#
# @API Security
# @internal
#
# TODO fill in the properties
# @model JWKs
# {
# "id": "JWKs",
# "description": "",
# "properties": {
# }
# }
#
class SecurityController < ApplicationController
# @API Show all available JWKs used by Canvas for signing.
#
# @returns JWKs
def jwks
key_storage = case request.path
when '/internal/services/jwks'
CanvasSecurity::ServicesJwt::KeyStorage
when '/login/oauth2/jwks'
Canvas::Oauth::KeyStorage
when '/api/lti/security/jwks'
Lti::KeyStorage
end
response.set_header('Cache-Control', "max-age=#{key_storage.max_cache_age}")
render json: key_storage.public_keyset
end
end

View File

@ -147,7 +147,7 @@ class DeveloperKey < ActiveRecord::Base
def generate_rsa_keypair!(overwrite: false)
return if public_jwk.present? && !overwrite
key_pair = Canvas::Security::RSAKeyPair.new
key_pair = CanvasSecurity::RSAKeyPair.new
@private_jwk = key_pair.to_jwk
self.public_jwk = key_pair.public_jwk.to_h
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require 'canvas_security'
# Register jwt token workflows with specific state requirments.
#
# - Try to keep workflow state in tokens to a minium. Remember this will be
# passed around with every request in the service workflow.
#
CanvasSecurity::JWTWorkflow.register(:rich_content) do |context, user|
tool_context = context.is_a?(Group) ? context.context : context
{
usage_rights_required: (
tool_context.respond_to?(:usage_rights_required?) &&
tool_context&.usage_rights_required?
) || false,
can_upload_files: (
user &&
context &&
context.grants_right?(user, :manage_files_add)
) || false,
can_create_pages: (
user &&
context &&
context.respond_to?(:wiki) &&
context.wiki_id &&
context.wiki.grants_right?(user, :create_page)
) || false
}
end
CanvasSecurity::JWTWorkflow.register(:ui) do |_, user|
{
use_high_contrast: user.try(:prefers_high_contrast?)
}
end

View File

@ -255,6 +255,10 @@ Rails.configuration.after_initialize do
Canvas::Oauth::KeyStorage.rotate_keys
end
Delayed::Periodic.cron 'CanvasSecurity::ServicesJwt::KeyStorage.rotate_keys', '0 0 1 * *', priority: Delayed::LOW_PRIORITY do
CanvasSecurity::KeyStorage.rotate_keys
end
Delayed::Periodic.cron 'Purgatory.expire_old_purgatories', '0 0 * * *', priority: Delayed::LOWER_PRIORITY do
with_each_shard_by_database(Purgatory, :expire_old_purgatories, local_offset: true)
end

View File

@ -942,6 +942,7 @@ CanvasRails::Application.routes.draw do
post 'object_snippet' => 'context#object_snippet'
get 'saml2' => 'login/saml#metadata'
get 'internal/services/jwks' => 'security#jwks'
# Routes for course exports
get 'xsd/:version.xsd' => 'content_exports#xml_schema'
@ -2391,7 +2392,7 @@ CanvasRails::Application.routes.draw do
post 'login/oauth2/accept' => 'oauth2_provider#accept', as: :oauth2_auth_accept
get 'login/oauth2/deny' => 'oauth2_provider#deny', as: :oauth2_auth_deny
delete 'login/oauth2/token' => 'oauth2_provider#destroy', as: :oauth2_logout
get 'login/oauth2/jwks' => 'oauth2_provider#jwks', as: :oauth2_jwks
get 'login/oauth2/jwks' => 'security#jwks', as: :oauth2_jwks
ApiRouteSet.draw(self, "/api/lti/v1") do
post "tools/:tool_id/grade_passback", controller: :lti_api, action: :grade_passback, as: "lti_grade_passback_api"
@ -2541,7 +2542,7 @@ CanvasRails::Application.routes.draw do
end
# Security
scope(controller: 'lti/ims/security') do
scope(controller: 'security') do
get "security/jwks", action: :jwks, as: :jwks_show
end

View File

@ -97,7 +97,7 @@ still a correctly signed token that's been subsequently encrypted.
You can verify the signature and decrypt upon receiving such a token:
```ruby
decrypted = CanvasSecurity.decrypt_services_jwt(jwt, signing_secret, encryption_secret)
decrypted = CanvasSecurity.decrypt_encrypted_jwt(jwt, signing_secret, encryption_secret)
```
This will error unless your JWT has a valid signature and can be decrypted.

View File

@ -22,7 +22,12 @@ require 'active_support/core_ext/module'
require 'json/jwt'
require 'dynamic_settings'
require 'canvas_errors'
require 'canvas_security/jwk_key_pair'
require 'canvas_security/jwt_workflow'
require 'canvas_security/key_storage'
require 'canvas_security/page_view_jwt'
require 'canvas_security/rsa_key_pair'
require 'canvas_security/services_jwt'
module CanvasSecurity
class UnconfiguredError < StandardError; end
@ -259,15 +264,7 @@ module CanvasSecurity
raise CanvasSecurity::InvalidToken
end
def self.decrypt_services_jwt(token, signing_secret = nil, encryption_secret = nil, ignore_expiration: false)
signing_secret ||= services_signing_secret
encryption_secret ||= services_encryption_secret
secrets_to_check = [signing_secret]
if signing_secret == services_signing_secret && services_previous_signing_secret
secrets_to_check << services_previous_signing_secret
end
def self.decrypt_encrypted_jwt(token, signing_secret, encryption_secret, ignore_expiration: false)
begin
signed_coded_jwt = JSON::JWT.decode(token, encryption_secret)
rescue OpenSSL::Cipher::CipherError => e
@ -277,6 +274,12 @@ module CanvasSecurity
raise CanvasSecurity::InvalidToken
end
secrets_to_check = if signing_secret.is_a?(Hash)
Array.wrap(signing_secret[JSON::JWT.decode(signed_coded_jwt.plain_text, :skip_verification).header['alg']])
else
Array.wrap(signing_secret)
end
secrets_to_check.each do |cur_secret|
begin
raw_jwt = JSON::JWT.decode(signed_coded_jwt.plain_text, cur_secret)

View File

@ -17,7 +17,7 @@
# 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::Security
module CanvasSecurity
class JWKKeyPair
attr_reader :public_key, :private_key, :alg, :use
@ -32,7 +32,7 @@ module Canvas::Security
private
def kid
@_kid ||= Time.now.utc.iso8601
@kid ||= "#{Time.now.utc.iso8601}_#{SecureRandom.uuid}"
end
end
end

View File

@ -17,7 +17,7 @@
# 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
module CanvasSecurity
class JWTWorkflow
def initialize(&token_state)
@token_state = token_state
@ -42,38 +42,5 @@ module Canvas
@workflows ||= {}
@workflows[label.to_sym] = JWTWorkflow.new(&token_state)
end
# Register jwt token workflows with specific state requirments.
#
# - Try to keep workflow state in tokens to a minium. Remember this will be
# passed around with every request in the service workflow.
#
register(:rich_content) do |context, user|
tool_context = context.is_a?(Group) ? context.context : context
{
usage_rights_required: (
tool_context.respond_to?(:usage_rights_required?) &&
tool_context&.usage_rights_required?
) || false,
can_upload_files: (
user &&
context &&
context.grants_right?(user, :manage_files_add)
) || false,
can_create_pages: (
user &&
context &&
context.respond_to?(:wiki) &&
context.wiki_id &&
context.wiki.grants_right?(user, :create_page)
) || false
}
end
register(:ui) do |_, user|
{
use_high_contrast: user.try(:prefers_high_contrast?)
}
end
end
end

View File

@ -18,7 +18,7 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module Canvas::Security
module CanvasSecurity
class KeyStorage
PAST = 'jwk-past.json'.freeze
PRESENT = 'jwk-present.json'.freeze
@ -61,17 +61,17 @@ module Canvas::Security
}
consul_proxy.set_keys(kvs, global: true)
end
Canvas::DynamicSettings.reset_cache!
DynamicSettings.reset_cache!
end
# Retrieve the public keys in JWK format
#
# @return [Array] The array of public keys in JWK format
def public_keyset
retrieve_keys.values.map do |private_jwk|
JSON::JWK::Set.new(retrieve_keys.values.compact.map do |private_jwk|
public_jwk = private_jwk.to_key.public_key.to_jwk
public_jwk.merge(private_jwk.select { |k, _| %w(alg use kid).include?(k) })
end
end)
end
# Retrieve the present key
@ -104,7 +104,7 @@ module Canvas::Security
end
def consul_proxy
@consul_proxy ||= Canvas::DynamicSettings.kv_proxy(@prefix, tree: :store)
@consul_proxy ||= DynamicSettings.kv_proxy(@prefix, tree: :store)
end
def self.max_cache_age
@ -112,7 +112,7 @@ module Canvas::Security
end
def self.new_key
Canvas::Security::RSAKeyPair.new.to_jwk.to_json
CanvasSecurity::RSAKeyPair.new.to_jwk.to_json
end
end
end

View File

@ -19,7 +19,7 @@
#
require 'openssl'
module Canvas::Security
module CanvasSecurity
class RSAKeyPair < JWKKeyPair
KTY = 'RSA'.freeze
ALG = 'RS256'.freeze

View File

@ -17,7 +17,9 @@
# 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/>.
class Canvas::Security::ServicesJwt
class CanvasSecurity::ServicesJwt
KeyStorage = CanvasSecurity::KeyStorage.new('services-jwt')
class InvalidRefresh < RuntimeError; end
REFRESH_WINDOW = 6.hours
@ -36,35 +38,20 @@ class Canvas::Security::ServicesJwt
def wrapper_token
return {} unless is_wrapped
raw_wrapper_token = Canvas::Security.base64_decode(token_string)
raw_wrapper_token = CanvasSecurity.base64_decode(token_string)
keys = [signing_secret]
keys << previous_signing_secret if previous_signing_secret
Canvas::Security.decode_jwt(raw_wrapper_token, keys)
CanvasSecurity.decode_jwt(raw_wrapper_token, keys)
end
def original_token(ignore_expiration: false)
original_crypted_token = if is_wrapped
wrapper_token[:user_token]
else
Canvas::Security.base64_decode(token_string)
CanvasSecurity.base64_decode(token_string)
end
Canvas::Security.decrypt_services_jwt(
CanvasSecurity::ServicesJwt.decrypt(
original_crypted_token,
signing_secret,
encryption_secret,
ignore_expiration: ignore_expiration
)
rescue Canvas::Security::InvalidToken
# if we failed during the wrapper token decoding then
# there is no way to decrypt this because we already
# tried the relevent keys, so we need not try anything else
# if original_crypted_token is nil.
raise unless original_crypted_token && previous_signing_secret
Canvas::Security.decrypt_services_jwt(
original_crypted_token,
previous_signing_secret,
encryption_secret,
ignore_expiration: ignore_expiration
)
end
@ -85,15 +72,25 @@ class Canvas::Security::ServicesJwt
original_token[:exp]
end
def self.generate(payload_data, base64 = true)
# Symmetric services JWTs are now deprecated
def self.generate(payload_data, base64 = true, symmetric: false)
payload = create_payload(payload_data)
crypted_token = Canvas::Security.create_encrypted_jwt(payload, signing_secret, encryption_secret)
crypted_token = if symmetric
CanvasSecurity.create_encrypted_jwt(payload, signing_secret, encryption_secret)
else
CanvasSecurity.create_encrypted_jwt(
payload,
CanvasSecurity::ServicesJwt::KeyStorage.present_key,
encryption_secret,
:autodetect
)
end
return crypted_token unless base64
Canvas::Security.base64_encode(crypted_token)
CanvasSecurity.base64_encode(crypted_token)
end
def self.for_user(domain, user, real_user: nil, workflows: nil, context: nil)
def self.for_user(domain, user, real_user: nil, workflows: nil, context: nil, symmetric: false)
if domain.blank? || user.nil?
raise ArgumentError, "Must have a domain and a user to build a JWT"
end
@ -105,17 +102,17 @@ class Canvas::Security::ServicesJwt
payload[:masq_sub] = real_user.global_id if real_user
if workflows.present?
payload[:workflows] = workflows
state = Canvas::JWTWorkflow.state_for(workflows, context, user)
state = CanvasSecurity::JWTWorkflow.state_for(workflows, context, user)
payload[:workflow_state] = state unless state.empty?
end
if context
payload[:context_type] = context.class.name
payload[:context_id] = context.id.to_s
end
generate(payload)
generate(payload, symmetric: symmetric)
end
def self.refresh_for_user(jwt, domain, user, real_user: nil)
def self.refresh_for_user(jwt, domain, user, real_user: nil, symmetric: false)
begin
payload = new(jwt, false).original_token(ignore_expiration: true)
rescue JSON::JWT::InvalidFormat
@ -137,7 +134,8 @@ class Canvas::Security::ServicesJwt
for_user(domain, user,
real_user: real_user,
workflows: payload[:workflows],
context: context)
context: context,
symmetric: symmetric)
end
def self.create_payload(payload_data)
@ -156,16 +154,23 @@ class Canvas::Security::ServicesJwt
})
end
def self.decrypt(token, ignore_expiration: false)
CanvasSecurity.decrypt_encrypted_jwt(token, {
'HS256' => [signing_secret, previous_signing_secret],
'RS256' => KeyStorage.public_keyset
}, encryption_secret, ignore_expiration: ignore_expiration)
end
def self.encryption_secret
Canvas::Security.services_encryption_secret
CanvasSecurity.services_encryption_secret
end
def self.signing_secret
Canvas::Security.services_signing_secret
CanvasSecurity.services_signing_secret
end
def self.previous_signing_secret
Canvas::Security.services_previous_signing_secret
CanvasSecurity.services_previous_signing_secret
end
private

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
RSpec.shared_context "services JWT wrapper" do
def build_wrapped_token(user_id, real_user_id: nil, encoding_secret: fake_signing_secret)
payload = { sub: user_id }
payload[:masq_sub] = real_user_id if real_user_id
crypted_token = CanvasSecurity::ServicesJwt.generate(payload, false, symmetric: true)
payload = {
iss: "some other service",
user_token: crypted_token
}
wrapper_token = CanvasSecurity.create_jwt(payload, nil, encoding_secret)
# because it will come over base64 encoded from any other service
CanvasSecurity.base64_encode(wrapper_token)
end
end
RSpec.shared_context "JWT setup" do
include_context "services JWT wrapper"
let(:fake_signing_secret) { "asdfasdfasdfasdfasdfasdfasdfasdf" }
let(:fake_encryption_secret) { "jkl;jkl;jkl;jkl;jkl;jkl;jkl;jkl;" }
let(:fake_secrets) do
{
"signing-secret" => fake_signing_secret,
"encryption-secret" => fake_encryption_secret
}
end
before do
allow(DynamicSettings).to receive(:find).with(any_args).and_call_original
allow(DynamicSettings).to receive(:find).with("canvas").and_return(fake_secrets)
end
after do
Timecop.return
end
around do |example|
Timecop.freeze(Time.utc(2013, 3, 13, 9, 12), &example)
end
end
RSpec.shared_context "JWT setup with deprecated secret" do
include_context "services JWT wrapper"
let(:fake_signing_secret) { "abcdefghijklmnopabcdefghijklmnop" }
let(:fake_encryption_secret) { "qrstuvwxyzqrstuvwxyzqrstuvwxyzqr" }
let(:fake_deprecated_signing_secret) { "nowiknowmyabcsnexttimewontyou..." }
let(:fake_secrets) do
{
"signing-secret" => fake_signing_secret,
"encryption-secret" => fake_encryption_secret,
"signing-secret-deprecated" => fake_deprecated_signing_secret
}
end
before do
allow(DynamicSettings).to receive(:find).with(any_args).and_call_original
allow(DynamicSettings).to receive(:find).with("canvas").and_return(fake_secrets)
end
after do
Timecop.return
end
around do |example|
Timecop.freeze(Time.utc(2021, 1, 11, 13, 21), &example)
end
end

View File

@ -18,14 +18,16 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require 'timecop'
require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
require 'spec_helper'
describe Canvas::Security::JWKKeyPair do
describe CanvasSecurity::JWKKeyPair do
describe "to_jwk" do
it 'has the private key in the JWK format' do
Timecop.freeze(Time.zone.now) do
keys = Canvas::Security::RSAKeyPair.new
expect(keys.to_jwk).to include(keys.private_key.to_jwk(kid: Time.now.utc.iso8601))
keys = CanvasSecurity::RSAKeyPair.new
jwk = keys.to_jwk
expect(jwk).to include(keys.private_key.to_jwk(kid: jwk['kid']))
expect(jwk['kid']).to include(Time.now.utc.iso8601)
end
end
end
@ -33,14 +35,16 @@ describe Canvas::Security::JWKKeyPair do
describe "public_jwk" do
it 'includes the public key in JWK format' do
Timecop.freeze(Time.zone.now) do
keys = Canvas::Security::RSAKeyPair.new
expect(keys.public_jwk).to include(keys.private_key.public_key.to_jwk(kid: Time.now.utc.iso8601))
keys = CanvasSecurity::RSAKeyPair.new
jwk = keys.public_jwk
expect(jwk).to include(keys.private_key.public_key.to_jwk(kid: jwk['kid']))
expect(jwk['kid']).to include(Time.now.utc.iso8601)
end
end
it 'does not include the private key claims in JWK format' do
Timecop.freeze(Time.zone.now) do
keys = Canvas::Security::RSAKeyPair.new
keys = CanvasSecurity::RSAKeyPair.new
expect(keys.public_jwk.keys).not_to include 'd', 'p', 'q', 'dp', 'dq', 'qi'
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require 'spec_helper'
describe CanvasSecurity::JWTWorkflow do
before(:each) do
@c = 'a_course'
@a = 'an_account'
end
describe 'register/state_for' do
it 'uses block registerd with workflow to build state' do
CanvasSecurity::JWTWorkflow.register(:foo) { |c, u| { c: c, u: u } }
state = CanvasSecurity::JWTWorkflow.state_for(%i[foo], @c, @u)
expect(state[:c]).to be(@c)
expect(state[:u]).to be(@u)
end
it 'returns an empty hash if if workflow is not registered' do
state = CanvasSecurity::JWTWorkflow.state_for(%i[not_defined], @c, @u)
expect(state).to be_empty
end
it 'merges state of muliple workflows in order of array' do
CanvasSecurity::JWTWorkflow.register(:foo) { { a: 1, b: 2 } }
CanvasSecurity::JWTWorkflow.register(:bar) { { b: 3, c: 4 } }
expect(CanvasSecurity::JWTWorkflow.state_for(%i[foo bar], nil, nil)).to include({ a: 1, b: 3, c: 4 })
expect(CanvasSecurity::JWTWorkflow.state_for(%i[bar foo], nil, nil)).to include({ a: 1, b: 2, c: 4 })
end
end
end

View File

@ -17,18 +17,19 @@
# 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 File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
describe Canvas::Security::KeyStorage do
require 'spec_helper'
describe CanvasSecurity::KeyStorage do
before do
@fallback_proxy = Canvas::DynamicSettings::FallbackProxy.new({
Canvas::Security::KeyStorage::PAST => Canvas::Security::KeyStorage.new_key,
Canvas::Security::KeyStorage::PRESENT => Canvas::Security::KeyStorage.new_key,
Canvas::Security::KeyStorage::FUTURE => Canvas::Security::KeyStorage.new_key
})
@fallback_proxy = DynamicSettings::FallbackProxy.new({
CanvasSecurity::KeyStorage::PAST => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::PRESENT => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::FUTURE => CanvasSecurity::KeyStorage.new_key
})
allow(Canvas::DynamicSettings).to receive(:kv_proxy).and_return(@fallback_proxy)
@key_storage = Canvas::Security::KeyStorage.new('mocked')
allow(DynamicSettings).to receive(:kv_proxy).and_return(@fallback_proxy)
@key_storage = CanvasSecurity::KeyStorage.new('mocked')
end
describe '#retrieve_keys_json' do
@ -45,22 +46,22 @@ describe Canvas::Security::KeyStorage do
it 'rotates the past key' do
keys_before = @key_storage.retrieve_keys
past = keys_before[Canvas::Security::KeyStorage::PAST].to_json
present = keys_before[Canvas::Security::KeyStorage::PRESENT].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[Canvas::Security::KeyStorage::PAST] }
past = keys_before[CanvasSecurity::KeyStorage::PAST].to_json
present = keys_before[CanvasSecurity::KeyStorage::PRESENT].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[CanvasSecurity::KeyStorage::PAST] }
.from(past).to(present)
end
it 'rotates the present key' do
keys_before = @key_storage.retrieve_keys
present = keys_before[Canvas::Security::KeyStorage::PRESENT].to_json
future = keys_before[Canvas::Security::KeyStorage::FUTURE].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[Canvas::Security::KeyStorage::PRESENT] }
present = keys_before[CanvasSecurity::KeyStorage::PRESENT].to_json
future = keys_before[CanvasSecurity::KeyStorage::FUTURE].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[CanvasSecurity::KeyStorage::PRESENT] }
.from(present).to(future)
end
it 'rotates the future key' do
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[Canvas::Security::KeyStorage::FUTURE] }
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[CanvasSecurity::KeyStorage::FUTURE] }
end
it 'initialize the keys if no keys are present' do
@ -68,15 +69,15 @@ describe Canvas::Security::KeyStorage do
@key_storage.rotate_keys
expect(
@fallback_proxy.data.values_at(
Canvas::Security::KeyStorage::PAST,
Canvas::Security::KeyStorage::PRESENT,
Canvas::Security::KeyStorage::FUTURE
CanvasSecurity::KeyStorage::PAST,
CanvasSecurity::KeyStorage::PRESENT,
CanvasSecurity::KeyStorage::FUTURE
)
).not_to include nil
end
it 'resets the cache after setting the keys' do
expect(Canvas::DynamicSettings).to receive(:reset_cache!)
expect(DynamicSettings).to receive(:reset_cache!)
@key_storage.rotate_keys
end
end
@ -85,17 +86,17 @@ describe Canvas::Security::KeyStorage do
keys_before = @key_storage.retrieve_keys
# We rely on the fact the the kid is the time the key was generated.
# Double-check that here.
future_key_time = Time.zone.parse(keys_before[Canvas::Security::KeyStorage::FUTURE]['kid'])
future_key_time = Time.zone.parse(keys_before[CanvasSecurity::KeyStorage::FUTURE]['kid'])
expect(future_key_time).to be_within(29).of(Time.zone.now)
Timecop.freeze(future_key_time + 59.minutes) do
expect { @key_storage.rotate_keys }.not_to change { @fallback_proxy.data[Canvas::Security::KeyStorage::PRESENT] }
expect { @key_storage.rotate_keys }.not_to change { @fallback_proxy.data[CanvasSecurity::KeyStorage::PRESENT] }
end
Timecop.freeze(future_key_time + 61.minutes) do
present = keys_before[Canvas::Security::KeyStorage::PRESENT].to_json
future = keys_before[Canvas::Security::KeyStorage::FUTURE].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[Canvas::Security::KeyStorage::PRESENT] }
present = keys_before[CanvasSecurity::KeyStorage::PRESENT].to_json
future = keys_before[CanvasSecurity::KeyStorage::FUTURE].to_json
expect { @key_storage.rotate_keys }.to change { @fallback_proxy.data[CanvasSecurity::KeyStorage::PRESENT] }
.from(present).to(future)
end
end
@ -104,11 +105,11 @@ describe Canvas::Security::KeyStorage do
describe "#public_keyset" do
it 'retrieve the public keys in JWK format' do
keys = @key_storage.retrieve_keys
expect(@key_storage.public_keyset.as_json).to eq([
select_public_claims(JSON::JWK.new(keys[Canvas::Security::KeyStorage::PAST])),
select_public_claims(JSON::JWK.new(keys[Canvas::Security::KeyStorage::PRESENT])),
select_public_claims(JSON::JWK.new(keys[Canvas::Security::KeyStorage::FUTURE]))
].as_json)
expect(JSON.parse(@key_storage.public_keyset.as_json.to_json)).to eq(JSON.parse({ keys: [
select_public_claims(JSON::JWK.new(keys[CanvasSecurity::KeyStorage::PAST])),
select_public_claims(JSON::JWK.new(keys[CanvasSecurity::KeyStorage::PRESENT])),
select_public_claims(JSON::JWK.new(keys[CanvasSecurity::KeyStorage::FUTURE]))
] }.to_json))
end
end

View File

@ -18,17 +18,17 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
require 'spec_helper'
describe Canvas::Security::RSAKeyPair do
describe CanvasSecurity::RSAKeyPair do
describe "initialize" do
it 'generates a public key of default size 2048' do
keys = Canvas::Security::RSAKeyPair.new
keys = CanvasSecurity::RSAKeyPair.new
expect(/\d+/.match(keys.public_key.to_text())[0]).to eq "2048"
end
it 'generates a private key of default size 2048' do
keys = Canvas::Security::RSAKeyPair.new
keys = CanvasSecurity::RSAKeyPair.new
expect(/\d+/.match(keys.private_key.to_text())[0]).to eq "2048"
end
end

View File

@ -17,18 +17,40 @@
# 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_relative '../../../spec_helper'
require_dependency "canvas/security/services_jwt"
require 'spec_helper'
module CanvasSecurity
# A dummy context
class ServicesJwtContext
attr_reader :id
def initialize(id)
@id = id
end
def self.find(id)
ServicesJwtContext.new(id)
end
end
module Canvas::Security
describe ServicesJwt do
before do
@fallback_proxy = DynamicSettings::FallbackProxy.new({
CanvasSecurity::KeyStorage::PAST => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::PRESENT => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::FUTURE => CanvasSecurity::KeyStorage.new_key
})
allow(DynamicSettings).to receive(:kv_proxy).and_return(@fallback_proxy)
end
describe "under normal circumstances" do
include_context "JWT setup"
let(:translate_token) do
->(jwt) {
decoded_crypted_token = Canvas::Security.base64_decode(jwt)
return Canvas::Security.decrypt_services_jwt(decoded_crypted_token)
decoded_crypted_token = CanvasSecurity.base64_decode(jwt)
return CanvasSecurity::ServicesJwt.decrypt(decoded_crypted_token)
}
end
@ -111,8 +133,8 @@ module Canvas::Security
it "allows the introduction of arbitrary data" do
jwt = ServicesJwt.generate(sub: 2, foo: "bar")
decoded_crypted_token = Canvas::Security.base64_decode(jwt)
decrypted_token_body = Canvas::Security.decrypt_services_jwt(decoded_crypted_token)
decoded_crypted_token = CanvasSecurity.base64_decode(jwt)
decrypted_token_body = CanvasSecurity::ServicesJwt.decrypt(decoded_crypted_token)
expect(decrypted_token_body[:foo]).to eq("bar")
end
@ -171,7 +193,7 @@ module Canvas::Security
it 'includes workflow_state if workflows is given' do
workflows = [:foo]
state = { 'a' => 123 }
expect(Canvas::JWTWorkflow).to receive(:state_for).with(workflows, ctx, user).and_return(state)
expect(CanvasSecurity::JWTWorkflow).to receive(:state_for).with(workflows, ctx, user).and_return(state)
jwt = ServicesJwt.for_user(host, user, workflows: workflows, context: ctx)
decrypted_token_body = translate_token.call(jwt)
expect(decrypted_token_body[:workflow_state]).to eq(state)
@ -179,18 +201,17 @@ module Canvas::Security
it 'does not include workflow_state if empty' do
workflows = [:foo]
expect(Canvas::JWTWorkflow).to receive(:state_for).and_return({})
expect(CanvasSecurity::JWTWorkflow).to receive(:state_for).and_return({})
jwt = ServicesJwt.for_user(host, user, workflows: workflows, context: ctx)
decrypted_token_body = translate_token.call(jwt)
expect(decrypted_token_body).not_to have_key :workflow_state
end
it 'includes context type and id if context is given' do
ctx = Course.new
ctx.id = 47
ctx = ServicesJwtContext.new(47)
jwt = ServicesJwt.for_user(host, user, context: ctx)
decrypted_token_body = translate_token.call(jwt)
expect(decrypted_token_body[:context_type]).to eq 'Course'
expect(decrypted_token_body[:context_type]).to eq 'CanvasSecurity::ServicesJwtContext'
expect(decrypted_token_body[:context_id]).to eq '47'
end
@ -273,7 +294,7 @@ module Canvas::Security
end
it 'generates a token with same context as original' do
context = course_factory
context = ServicesJwtContext.new(123)
jwt = ServicesJwt.for_user(host, user1, context: context)
refreshed = ServicesJwt.refresh_for_user(jwt, host, user1)
payload = translate_token.call(refreshed)
@ -318,10 +339,10 @@ module Canvas::Security
jwt_non_key = ServicesJwt.new(base64_encoded_wrapper_non_key)
expect(jwt_new_key.wrapper_token[:iss]).to eq("some other service")
expect(jwt_old_key.wrapper_token[:iss]).to eq("some other service")
expect { jwt_non_key.wrapper_token }.to raise_error(Canvas::Security::InvalidToken)
expect { jwt_non_key.wrapper_token }.to raise_error(CanvasSecurity::InvalidToken)
expect(jwt_new_key.user_global_id).to eq(84)
expect(jwt_old_key.user_global_id).to eq(84)
expect { jwt_non_key.user_global_id }.to raise_error(Canvas::Security::InvalidToken)
expect { jwt_non_key.user_global_id }.to raise_error(CanvasSecurity::InvalidToken)
end
end
end

View File

@ -92,7 +92,7 @@ describe CanvasSecurity do
it "can unpack encrypted jwts again" do
jwt = CanvasSecurity.create_encrypted_jwt(payload, signing_secret, encryption_secret)
original = CanvasSecurity.decrypt_services_jwt(jwt, signing_secret, encryption_secret)
original = CanvasSecurity.decrypt_encrypted_jwt(jwt, signing_secret, encryption_secret)
expect(original[:arbitrary]).to eq("data")
end
@ -100,7 +100,7 @@ describe CanvasSecurity do
different_secret = encryption_secret.upcase
jwt = CanvasSecurity.create_encrypted_jwt(payload, signing_secret, different_secret)
expect do
CanvasSecurity.decrypt_services_jwt(jwt, signing_secret, encryption_secret)
CanvasSecurity.decrypt_encrypted_jwt(jwt, signing_secret, encryption_secret)
end.to raise_error(CanvasSecurity::InvalidToken)
end
end

View File

@ -21,6 +21,7 @@ require 'byebug'
require 'canvas_security'
require 'rails'
Rails.env = 'test'
Time.zone = 'UTC'
# Right now Canvas injects the Setting class as the store.
# It would be great to pull that one out to something we can
@ -45,6 +46,8 @@ class MemorySettings
end
CanvasSecurity.settings_store = MemorySettings.new
require 'canvas_security/spec/jwt_env'
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true

View File

@ -67,7 +67,7 @@ module AuthenticationMethods
return if load_pseudonym_from_inst_access_token(token_string)
begin
services_jwt = Canvas::Security::ServicesJwt.new(token_string)
services_jwt = CanvasSecurity::ServicesJwt.new(token_string)
@current_user = User.find(services_jwt.user_global_id)
@current_pseudonym = SisPseudonym.for(@current_user, @domain_root_account, type: :implicit, require_sis: false)
unless @current_user && @current_pseudonym

View File

@ -96,7 +96,7 @@ module BasicLTI
end
def self.token_from_sourcedid!(sourcedid)
Canvas::Security.decrypt_services_jwt(
Canvas::Security.decrypt_encrypted_jwt(
Canvas::Security.base64_decode(sourcedid),
signing_secret,
encryption_secret

View File

@ -19,5 +19,5 @@
#
module Canvas::Oauth
KeyStorage = Canvas::Security::KeyStorage.new('oauth2-asymmetric')
KeyStorage = CanvasSecurity::KeyStorage.new('oauth2-asymmetric')
end

View File

@ -19,5 +19,5 @@
#
module Lti
KeyStorage = Canvas::Security::KeyStorage.new('lti-keys')
KeyStorage = CanvasSecurity::KeyStorage.new('lti-keys')
end

View File

@ -26,11 +26,11 @@ module Schemas::Lti
'properties' => {
'kty' => {
'type' => 'string',
'const' => Canvas::Security::RSAKeyPair::KTY
'const' => CanvasSecurity::RSAKeyPair::KTY
}.freeze,
'alg' => {
'type' => 'string',
'const' => Canvas::Security::RSAKeyPair::ALG
'const' => CanvasSecurity::RSAKeyPair::ALG
}.freeze,
'e' => {
'type' => 'string'

View File

@ -102,7 +102,7 @@ module Services
end
def headers(jwt_body, headers = {})
token = Canvas::Security::ServicesJwt.generate(jwt_body)
token = CanvasSecurity::ServicesJwt.generate(jwt_body, symmetric: true)
headers['Authorization'] = "Bearer #{token}"
headers
end

View File

@ -23,12 +23,14 @@ module Services
env_hash = service_settings.dup
if user && domain
begin
env_hash[:JWT] = Canvas::Security::ServicesJwt.for_user(
env_hash[:JWT] = CanvasSecurity::ServicesJwt.for_user(
domain,
user,
context: context,
real_user: real_user,
workflows: [:rich_content, :ui]
workflows: [:rich_content, :ui],
# TODO: remove this once we teach the rcs to consume the asymmetric ones
symmetric: true
)
rescue Canvas::Security::InvalidJwtKey => exception
Canvas::Errors.capture_exception(:jwt, exception)

View File

@ -595,7 +595,7 @@ describe "API Authentication", type: :request do
end
def wrapped_jwt_from_service(payload = { sub: @user.global_id })
services_jwt = Canvas::Security::ServicesJwt.generate(payload, false)
services_jwt = CanvasSecurity::ServicesJwt.generate(payload, false, symmetric: true)
payload = {
iss: "some other service",
user_token: services_jwt

View File

@ -27,7 +27,7 @@ describe JwtsController do
->(resp) {
utf8_token_string = json_parse(resp.body)['token']
decoded_crypted_token = Canvas::Security.base64_decode(utf8_token_string)
return Canvas::Security.decrypt_services_jwt(decoded_crypted_token)
return CanvasSecurity::ServicesJwt.decrypt(decoded_crypted_token)
}
end
@ -93,9 +93,9 @@ describe JwtsController do
real_user = site_admin_user(active_user: true)
user_with_pseudonym(:user => other_user, :username => "other@example.com")
user_session(real_user)
services_jwt = class_double(Canvas::Security::ServicesJwt).as_stubbed_const
services_jwt = class_double(CanvasSecurity::ServicesJwt).as_stubbed_const
expect(services_jwt).to receive(:refresh_for_user)
.with('testjwt', 'testhost', other_user, real_user: real_user)
.with('testjwt', 'testhost', other_user, real_user: real_user, symmetric: true)
.and_return('refreshedjwt')
post 'refresh', params: { jwt: 'testjwt', as_user_id: other_user.id }, format: 'json'
token = JSON.parse(response.body)['token']
@ -104,9 +104,10 @@ describe JwtsController do
it "returns a different jwt when refresh is called" do
course = course_factory
original_jwt = Canvas::Security::ServicesJwt.for_user(
original_jwt = CanvasSecurity::ServicesJwt.for_user(
request.host_with_port,
token_user
token_user,
symmetric: true
)
post 'refresh', params: { jwt: original_jwt }
refreshed_jwt = JSON.parse(response.body)['token']
@ -114,10 +115,10 @@ describe JwtsController do
end
it "returns an error if jwt is invalid for refresh" do
services_jwt = class_double(Canvas::Security::ServicesJwt)
services_jwt = class_double(CanvasSecurity::ServicesJwt)
.as_stubbed_const(transfer_nested_constants: true)
expect(services_jwt).to receive(:refresh_for_user)
.and_raise(Canvas::Security::ServicesJwt::InvalidRefresh)
.and_raise(CanvasSecurity::ServicesJwt::InvalidRefresh)
post 'refresh', params: { jwt: 'testjwt' }, format: 'json'
expect(response.status).to eq(400)
end

View File

@ -40,8 +40,8 @@ describe Lti::DataServicesController do
end
before do
allow(Canvas::Security::ServicesJwt).to receive(:encryption_secret).and_return('setecastronomy92' * 2)
allow(Canvas::Security::ServicesJwt).to receive(:signing_secret).and_return('donttell' * 10)
allow(CanvasSecurity::ServicesJwt).to receive(:encryption_secret).and_return('setecastronomy92' * 2)
allow(CanvasSecurity::ServicesJwt).to receive(:signing_secret).and_return('donttell' * 10)
allow(HTTParty).to receive(:send).and_return(double(body: subscription, code: 200))
root_account.lti_context_id = SecureRandom.uuid

View File

@ -163,7 +163,7 @@ module Lti
end
context 'when a url is used to get public key' do
let(:rsa_key_pair) { Canvas::Security::RSAKeyPair.new }
let(:rsa_key_pair) { CanvasSecurity::RSAKeyPair.new }
let(:url) { "https://get.public.jwk" }
let(:public_jwk_url_response) do
{

View File

@ -1,55 +0,0 @@
# frozen_string_literal: true
#
# Copyright (C) 2018 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../../../lti_1_3_spec_helper')
require_dependency "lti/ims/security_controller"
module Lti::Ims
RSpec.describe SecurityController, type: :request do
include_context 'lti_1_3_spec_helper'
let(:url) { Rails.application.routes.url_helpers.jwks_show_path }
let(:json) { JSON.parse(response.body) }
it 'returns ok status' do
get url
expect(response).to have_http_status :ok
end
it 'returns a jwk set' do
get url
expect(json['keys']).not_to be_empty
end
it 'sets the Cache-control header' do
get url
expect(response.headers['Cache-Control']).to include 'max-age=864000'
end
it 'returns well-formed public key jwks' do
get url
expected_keys = %w(kid kty alg e n use)
json['keys'].each do |key|
expect(key.keys - expected_keys).to be_empty
end
end
end
end

View File

@ -32,7 +32,7 @@ module Lti
let(:expected_mime_type) { described_class::MIME_TYPE }
let(:scope_to_remove) { "https://canvas.instructure.com/lti/public_jwk/scope/update" }
let(:new_public_jwk) do
key_hash = Canvas::Security::RSAKeyPair.new.public_jwk.to_h
key_hash = CanvasSecurity::RSAKeyPair.new.public_jwk.to_h
key_hash['kty'] = key_hash['kty'].to_s
key_hash
end
@ -47,7 +47,7 @@ module Lti
let(:action) { :update }
let(:old_public_jwk) { developer_key.public_jwk }
let(:new_public_jwk) do
key_hash = Canvas::Security::RSAKeyPair.new.public_jwk.to_h
key_hash = CanvasSecurity::RSAKeyPair.new.public_jwk.to_h
key_hash['kty'] = key_hash['kty'].to_s
key_hash
end

View File

@ -838,25 +838,4 @@ describe Oauth2ProviderController do
end
end
end
describe 'GET jwks' do
before :each do
allow(Canvas::Oauth::KeyStorage).to receive(:retrieve_keys).and_return({
Canvas::Security::KeyStorage::PAST => Canvas::Security::RSAKeyPair.new.to_jwk,
Canvas::Security::KeyStorage::PRESENT => Canvas::Security::RSAKeyPair.new.to_jwk,
Canvas::Security::KeyStorage::FUTURE => Canvas::Security::RSAKeyPair.new.to_jwk
})
get 'jwks'
end
it "provides the current jwk set" do
expect(response).to have_http_status :ok
json = JSON.parse(response.body)
expected_keys = %w(kid kty alg e n use)
expect(json['keys']).not_to be_empty
json['keys'].each do |key|
expect(key.keys - expected_keys).to be_empty
end
end
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
#
# Copyright (C) 2018 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
RSpec.describe SecurityController, type: :request do
# This uses the lti keyset, but it doesn't really matter which one
let(:url) { Rails.application.routes.url_helpers.jwks_show_path }
let(:json) { JSON.parse(response.body) }
let(:fallback_proxy) do
Canvas::DynamicSettings::FallbackProxy.new({
CanvasSecurity::KeyStorage::PAST => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::PRESENT => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::FUTURE => CanvasSecurity::KeyStorage.new_key
})
end
before do
allow(Canvas::DynamicSettings).to receive(:kv_proxy).and_return(fallback_proxy)
end
it 'returns ok status' do
get url
expect(response).to have_http_status :ok
end
it 'returns a jwk set' do
get url
expect(json['keys']).not_to be_empty
end
it 'sets the Cache-control header' do
get url
expect(response.headers['Cache-Control']).to include 'max-age=864000'
end
it 'returns well-formed public key jwks' do
get url
expected_keys = %w(kid kty alg e n use)
json['keys'].each do |key|
expect(key.keys - expected_keys).to be_empty
end
end
end

View File

@ -0,0 +1,127 @@
# frozen_string_literal: true
#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require 'spec_helper'
describe CanvasSecurity::JWTWorkflow do
before(:each) do
@c = Course.new
@a = Account.new
@c.account = @a
@c.root_account = @a
@u = User.new
@a.save!
@u.save!
@c.save!
@g = Group.new
@g.context = @c
@g.save!
@g.add_user(@u)
end
describe 'workflows' do
describe ':rich_content' do
before(:each) do
allow(@c).to receive(:respond_to?).with(:usage_rights_required?).and_return(true)
allow(@c).to receive(:grants_right?)
allow(@c).to receive(:feature_enabled?)
@wiki = Wiki.new
allow(@c).to receive(:wiki).and_return(@wiki)
allow(@c).to receive(:respond_to?).with(:wiki).and_return(true)
allow(@wiki).to receive(:grants_right?)
allow(@g).to receive(:can_participate).and_return(true)
end
it 'sets can_upload_files to false' do
expect(@c).to receive(:grants_right?).with(@u, :manage_files_add).and_return(false)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_upload_files]).to be false
end
it 'sets can_upload_files to true' do
expect(@c).to receive(:grants_right?).with(@u, :manage_files_add).and_return(true)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_upload_files]).to be true
end
it 'sets usage_rights_required to false' do
@c.usage_rights_required = false
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:usage_rights_required]).to be false
end
it 'sets usage_rights_required to true' do
@c.usage_rights_required = true
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:usage_rights_required]).to be true
end
it 'sets group usage_rights_required to false if false on its course' do
@c.usage_rights_required = false
state = described_class.state_for(%i[rich_content], @g, @u)
expect(state[:usage_rights_required]).to be false
end
it 'sets group usage_rights_required to true if true on its course' do
@c.usage_rights_required = true
state = described_class.state_for(%i[rich_content], @g, @u)
expect(state[:usage_rights_required]).to be true
end
it 'sets can_create_pages to false if context does not have a wiki' do
expect(@c).to receive(:respond_to?).with(:wiki).and_return(false)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
expect(@c).to receive(:wiki_id).and_return(nil)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
end
it 'sets can_create_pages to false if user does not have create_page rights' do
@c.wiki_id = 1
expect(@wiki).to receive(:grants_right?).with(@u, :create_page).and_return(false)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
end
it 'sets can_create_pages to true if user has create_page rights' do
@c.wiki_id = 1
expect(@wiki).to receive(:grants_right?).with(@u, :create_page).and_return(true)
state = described_class.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be true
end
end
describe ':ui' do
before(:each) { allow(@u).to receive(:prefers_high_contrast?) }
it 'sets use_high_contrast to true' do
expect(@u).to receive(:prefers_high_contrast?).and_return(true)
state = described_class.state_for(%i[ui], @c, @u)
expect(state[:use_high_contrast]).to be true
end
it 'sets use_high_contrast to false' do
expect(@u).to receive(:prefers_high_contrast?).and_return(false)
state = described_class.state_for(%i[ui], @c, @u)
expect(state[:use_high_contrast]).to be false
end
end
end
end

View File

@ -142,7 +142,7 @@ describe AuthenticationMethods do
def build_encoded_token(user_id, real_user_id: nil)
payload = { sub: user_id }
payload[:masq_sub] = real_user_id if real_user_id
crypted_token = Canvas::Security::ServicesJwt.generate(payload, false)
crypted_token = CanvasSecurity::ServicesJwt.generate(payload, false, symmetric: true)
payload = {
iss: "some other service",
user_token: crypted_token

View File

@ -1,151 +0,0 @@
# frozen_string_literal: true
#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# 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/>.
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
require_dependency 'canvas/jwt_workflow'
module Canvas
describe JWTWorkflow do
before(:each) do
@c = Course.new
@a = Account.new
@c.account = @a
@c.root_account = @a
@u = User.new
@a.save!
@u.save!
@c.save!
@g = Group.new
@g.context = @c
@g.save!
@g.add_user(@u)
end
describe 'register/state_for' do
it 'uses block registerd with workflow to build state' do
JWTWorkflow.register(:foo) { |c, u| { c: c, u: u } }
state = JWTWorkflow.state_for(%i[foo], @c, @u)
expect(state[:c]).to be(@c)
expect(state[:u]).to be(@u)
end
it 'returns an empty hash if if workflow is not registered' do
state = JWTWorkflow.state_for(%i[not_defined], @c, @u)
expect(state).to be_empty
end
it 'merges state of muliple workflows in order of array' do
JWTWorkflow.register(:foo) { { a: 1, b: 2 } }
JWTWorkflow.register(:bar) { { b: 3, c: 4 } }
expect(JWTWorkflow.state_for(%i[foo bar], nil, nil)).to include({ a: 1, b: 3, c: 4 })
expect(JWTWorkflow.state_for(%i[bar foo], nil, nil)).to include({ a: 1, b: 2, c: 4 })
end
end
describe 'workflows' do
describe ':rich_content' do
before(:each) do
allow(@c).to receive(:respond_to?).with(:usage_rights_required?).and_return(true)
allow(@c).to receive(:grants_right?)
allow(@c).to receive(:feature_enabled?)
@wiki = Wiki.new
allow(@c).to receive(:wiki).and_return(@wiki)
allow(@c).to receive(:respond_to?).with(:wiki).and_return(true)
allow(@wiki).to receive(:grants_right?)
allow(@g).to receive(:can_participate).and_return(true)
end
it 'sets can_upload_files to false' do
expect(@c).to receive(:grants_right?).with(@u, :manage_files_add).and_return(false)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_upload_files]).to be false
end
it 'sets can_upload_files to true' do
expect(@c).to receive(:grants_right?).with(@u, :manage_files_add).and_return(true)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_upload_files]).to be true
end
it 'sets usage_rights_required to false' do
@c.usage_rights_required = false
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:usage_rights_required]).to be false
end
it 'sets usage_rights_required to true' do
@c.usage_rights_required = true
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:usage_rights_required]).to be true
end
it 'sets group usage_rights_required to false if false on its course' do
@c.usage_rights_required = false
state = JWTWorkflow.state_for(%i[rich_content], @g, @u)
expect(state[:usage_rights_required]).to be false
end
it 'sets group usage_rights_required to true if true on its course' do
@c.usage_rights_required = true
state = JWTWorkflow.state_for(%i[rich_content], @g, @u)
expect(state[:usage_rights_required]).to be true
end
it 'sets can_create_pages to false if context does not have a wiki' do
expect(@c).to receive(:respond_to?).with(:wiki).and_return(false)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
expect(@c).to receive(:wiki_id).and_return(nil)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
end
it 'sets can_create_pages to false if user does not have create_page rights' do
@c.wiki_id = 1
expect(@wiki).to receive(:grants_right?).with(@u, :create_page).and_return(false)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be false
end
it 'sets can_create_pages to true if user has create_page rights' do
@c.wiki_id = 1
expect(@wiki).to receive(:grants_right?).with(@u, :create_page).and_return(true)
state = JWTWorkflow.state_for(%i[rich_content], @c, @u)
expect(state[:can_create_pages]).to be true
end
end
describe ':ui' do
before(:each) { allow(@u).to receive(:prefers_high_contrast?) }
it 'sets use_high_contrast to true' do
expect(@u).to receive(:prefers_high_contrast?).and_return(true)
state = JWTWorkflow.state_for(%i[ui], @c, @u)
expect(state[:use_high_contrast]).to be true
end
it 'sets use_high_contrast to false' do
expect(@u).to receive(:prefers_high_contrast?).and_return(false)
state = JWTWorkflow.state_for(%i[ui], @c, @u)
expect(state[:use_high_contrast]).to be false
end
end
end
end
end

View File

@ -73,7 +73,7 @@ module Canvas::Oauth
let(:aud) { Rails.application.routes.url_helpers.oauth2_token_url }
let(:iat) { 1.minute.ago.to_i }
let(:exp) { 10.minutes.from_now.to_i }
let(:rsa_key_pair) { Canvas::Security::RSAKeyPair.new }
let(:rsa_key_pair) { CanvasSecurity::RSAKeyPair.new }
let(:signing_key) { JSON::JWK.new(rsa_key_pair.to_jwk) }
let(:jwt) do
{
@ -219,7 +219,7 @@ module Canvas::Oauth
end
context 'with bad signing key' do
let(:signing_key) { JSON::JWK.new(Canvas::Security::RSAKeyPair.new.to_jwk) }
let(:signing_key) { JSON::JWK.new(CanvasSecurity::RSAKeyPair.new.to_jwk) }
it { is_expected.not_to be_empty }
end

View File

@ -108,7 +108,7 @@ module Services
expect(HTTParty).to receive(:send) do |method, endpoint, options|
expect(method).to eq(:delete)
expect(endpoint).to eq('http://example.com/api/subscriptions')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt["DeveloperKey"]).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000004')
expect(jwt["sub"]).to eq('ltiToolProxy:151b52cd-d670-49fb-bf65-6a327e3aaca0')
@ -124,7 +124,7 @@ module Services
expect(HTTParty).to receive(:send) do |method, endpoint, options|
expect(method).to eq(:delete)
expect(endpoint).to eq('http://example.com/api/subscriptions/subscription_id')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt["DeveloperKey"]).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000004')
expect(jwt["RootAccountUUID"]).to eq('random-account-uuid')
@ -141,7 +141,7 @@ module Services
expect(HTTParty).to receive(:send) do |method, endpoint, options|
expect(method).to eq(:get)
expect(endpoint).to eq('http://example.com/api/subscriptions/subscription_id')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt["DeveloperKey"]).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000007')
expect(jwt["RootAccountUUID"]).to eq('random-account-uuid')
@ -158,7 +158,7 @@ module Services
expect(HTTParty).to receive(:send) do |method, endpoint, options|
expect(method).to eq(:get)
expect(endpoint).to eq('http://example.com/api/root_account_subscriptions')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt["DeveloperKey"]).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000007')
expect(jwt["RootAccountUUID"]).to eq('random-account-uuid')
@ -178,7 +178,7 @@ module Services
expect(method).to eq(:post)
expect(endpoint).to eq('http://example.com/api/subscriptions')
expect(options[:headers]['Content-Type']).to eq('application/json')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt['DeveloperKey']).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000004')
expect(jwt["RootAccountUUID"]).to eq('random-account-uuid')
@ -200,7 +200,7 @@ module Services
expect(method).to eq(:put)
expect(endpoint).to eq('http://example.com/api/subscriptions/1234')
expect(options[:headers]['Content-Type']).to eq('application/json')
jwt = Canvas::Security::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
jwt = CanvasSecurity::ServicesJwt.new(options[:headers]['Authorization'].gsub('Bearer ', ''), false).original_token
expect(jwt['DeveloperKey']).to eq('10000000000003')
expect(jwt["RootAccountId"]).to eq('10000000000004')
expect(jwt["RootAccountUUID"]).to eq('random-account-uuid')

View File

@ -44,9 +44,9 @@ module Services
domain = double("domain")
ctx = double("ctx", grants_right?: true)
jwt = double("jwt")
allow(Canvas::Security::ServicesJwt).to receive(:for_user).with(domain, user,
include(workflows: [:rich_content, :ui],
context: ctx)).and_return(jwt)
allow(CanvasSecurity::ServicesJwt).to receive(:for_user).with(domain, user,
include(workflows: [:rich_content, :ui],
context: ctx)).and_return(jwt)
env = described_class.env_for(user: user, domain: domain, context: ctx)
expect(env[:JWT]).to eql(jwt)
end
@ -56,7 +56,7 @@ module Services
masq_user = double("masq_user", global_id: 'other global id')
domain = double("domain")
jwt = double("jwt")
allow(Canvas::Security::ServicesJwt).to receive(:for_user).with(
allow(CanvasSecurity::ServicesJwt).to receive(:for_user).with(
domain,
user,
include(real_user: masq_user),
@ -82,7 +82,7 @@ module Services
end
it "does not raise when encyption/signing secrets are nil" do
allow(Canvas::Security::ServicesJwt).to receive(:for_user).and_raise(Canvas::Security::InvalidJwtKey)
allow(CanvasSecurity::ServicesJwt).to receive(:for_user).and_raise(Canvas::Security::InvalidJwtKey)
env = described_class.env_for(user: {}, domain: "domain")
expect(env[:JWT]).to eq("InvalidJwtKey")
end

View File

@ -25,9 +25,9 @@ RSpec.shared_context "lti_1_3_spec_helper", shared_context: :metadata do
let(:fallback_proxy) do
Canvas::DynamicSettings::FallbackProxy.new({
Canvas::Security::KeyStorage::PAST => Canvas::Security::KeyStorage.new_key,
Canvas::Security::KeyStorage::PRESENT => Canvas::Security::KeyStorage.new_key,
Canvas::Security::KeyStorage::FUTURE => Canvas::Security::KeyStorage.new_key
CanvasSecurity::KeyStorage::PAST => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::PRESENT => CanvasSecurity::KeyStorage.new_key,
CanvasSecurity::KeyStorage::FUTURE => CanvasSecurity::KeyStorage.new_key
})
end

View File

@ -65,7 +65,7 @@ describe DeveloperKey do
describe 'default values for is_lti_key' do
let(:public_jwk) do
key_hash = Canvas::Security::RSAKeyPair.new.public_jwk.to_h
key_hash = CanvasSecurity::RSAKeyPair.new.public_jwk.to_h
key_hash['kty'] = key_hash['kty'].to_s
key_hash
end
@ -955,11 +955,11 @@ describe DeveloperKey do
before { subject.generate_rsa_keypair! }
it 'populates the "public_jwk" column with a public key' do
expect(subject.public_jwk['kty']).to eq Canvas::Security::RSAKeyPair::KTY
expect(subject.public_jwk['kty']).to eq CanvasSecurity::RSAKeyPair::KTY
end
it 'populates the "private_jwk" attribute with a private key' do
expect(subject.private_jwk['kty']).to eq Canvas::Security::RSAKeyPair::KTY.to_sym
expect(subject.private_jwk['kty']).to eq CanvasSecurity::RSAKeyPair::KTY.to_sym
end
end
end
@ -1081,7 +1081,7 @@ describe DeveloperKey do
describe "issue_token" do
subject { DeveloperKey.create! }
let(:claims) { { "key" => "value" } }
let(:asymmetric_keypair) { Canvas::Security::RSAKeyPair.new.to_jwk }
let(:asymmetric_keypair) { CanvasSecurity::RSAKeyPair.new.to_jwk }
let(:asymmetric_public_key) { asymmetric_keypair.to_key.public_key.to_jwk }
before {

View File

@ -17,71 +17,4 @@
# 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/>.
RSpec.shared_context "services JWT wrapper" do
def build_wrapped_token(user_id, real_user_id: nil, encoding_secret: fake_signing_secret)
payload = { sub: user_id }
payload[:masq_sub] = real_user_id if real_user_id
crypted_token = Canvas::Security::ServicesJwt.generate(payload, false)
payload = {
iss: "some other service",
user_token: crypted_token
}
wrapper_token = Canvas::Security.create_jwt(payload, nil, encoding_secret)
# because it will come over base64 encoded from any other service
Canvas::Security.base64_encode(wrapper_token)
end
end
RSpec.shared_context "JWT setup" do
include_context "services JWT wrapper"
let(:fake_signing_secret) { "asdfasdfasdfasdfasdfasdfasdfasdf" }
let(:fake_encryption_secret) { "jkl;jkl;jkl;jkl;jkl;jkl;jkl;jkl;" }
let(:fake_secrets) {
{
"signing-secret" => fake_signing_secret,
"encryption-secret" => fake_encryption_secret
}
}
before do
allow(Canvas::DynamicSettings).to receive(:find).with(any_args).and_call_original
allow(Canvas::DynamicSettings).to receive(:find).with("canvas").and_return(fake_secrets)
end
after do
Timecop.return
end
around do |example|
Timecop.freeze(Time.utc(2013, 3, 13, 9, 12), &example)
end
end
RSpec.shared_context "JWT setup with deprecated secret" do
include_context "services JWT wrapper"
let(:fake_signing_secret) { "abcdefghijklmnopabcdefghijklmnop" }
let(:fake_encryption_secret) { "qrstuvwxyzqrstuvwxyzqrstuvwxyzqr" }
let(:fake_deprecated_signing_secret) { "nowiknowmyabcsnexttimewontyou..." }
let(:fake_secrets) {
{
"signing-secret" => fake_signing_secret,
"encryption-secret" => fake_encryption_secret,
"signing-secret-deprecated" => fake_deprecated_signing_secret
}
}
before do
allow(Canvas::DynamicSettings).to receive(:find).with(any_args).and_call_original
allow(Canvas::DynamicSettings).to receive(:find).with("canvas").and_return(fake_secrets)
end
after do
Timecop.return
end
around do |example|
Timecop.freeze(Time.utc(2021, 1, 11, 13, 21), &example)
end
end
require 'canvas_security/spec/jwt_env'