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:
parent
121d12055d
commit
214014049f
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -19,7 +19,7 @@
|
|||
#
|
||||
require 'openssl'
|
||||
|
||||
module Canvas::Security
|
||||
module CanvasSecurity
|
||||
class RSAKeyPair < JWKKeyPair
|
||||
KTY = 'RSA'.freeze
|
||||
ALG = 'RS256'.freeze
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,5 +19,5 @@
|
|||
#
|
||||
|
||||
module Canvas::Oauth
|
||||
KeyStorage = Canvas::Security::KeyStorage.new('oauth2-asymmetric')
|
||||
KeyStorage = CanvasSecurity::KeyStorage.new('oauth2-asymmetric')
|
||||
end
|
||||
|
|
|
@ -19,5 +19,5 @@
|
|||
#
|
||||
|
||||
module Lti
|
||||
KeyStorage = Canvas::Security::KeyStorage.new('lti-keys')
|
||||
KeyStorage = CanvasSecurity::KeyStorage.new('lti-keys')
|
||||
end
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -44,7 +44,7 @@ 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,
|
||||
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)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue