Class: PandaPal::Session
- Inherits:
-
PandaPalRecord
- Object
- ActiveRecord::Base
- PandaPalRecord
- PandaPal::Session
- Defined in:
- app/models/panda_pal/session.rb
Defined Under Namespace
Classes: DataSerializer, RoleStore, SessionNonceMismatch
Class Method Summary collapse
- .extract_panda_token(request, params = request.params) ⇒ Object
- .for_panda_token(panda_token, request: nil, validate: true, enforce_tenant: true) ⇒ Object
- .for_request(request, params = request.params, enforce_tenant: true) ⇒ Object
- .format_url_for_signing(uri) ⇒ Object
Instance Method Summary collapse
- #[](key) ⇒ Object
- #[]=(key, value) ⇒ Object
- #cache(key, &blk) ⇒ Object
-
#canvas_account_role_labels(account = 'self') ⇒ Object
Retrieve the User’s Role Labels in the specified Account, defaulting to the Root Account.
-
#canvas_role_labels ⇒ Object
Retrieve the User’s Role Labels in the Launch Context.
- #canvas_site_admin? ⇒ Boolean
- #canvas_user_id ⇒ Object
- #custom_lti_params ⇒ Object
- #get_lti_cust_param(key, default: :if_not_var) ⇒ Object
- #launch_params ⇒ Object
- #lti_launch_placement ⇒ Object
- #lti_platform ⇒ Object
- #lti_roles ⇒ Object
- #session_key ⇒ Object
- #sign_value(data) ⇒ Object
- #signing_key ⇒ Object
- #user ⇒ Object
- #validate_signature(data, signature) ⇒ Object
Class Method Details
.extract_panda_token(request, params = request.params) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'app/models/panda_pal/session.rb', line 106 def self.extract_panda_token(request, params = request.params) headers = request.respond_to?(:headers) ? request.headers : request.env token = headers['HTTP_X_PANDA_TOKEN'] || headers['X-Panda-Token'] token ||= begin if (auth_header = headers['HTTP_AUTHORIZATION'] || headers['Authorization']).present? match = auth_header.match(/Bearer panda:(.+)/) match ||= auth_header.match(/token=(.+)/) # Legacy Support token = match[1] if match && match[1].include?('.') end end token ||= params["panda_token"] token ||= params["session_token"] if params["session_token"]&.include?('.') # Legacy Support token ||= params["session_key"] if params["session_key"]&.include?('.') # Legacy Support token.presence end |
.for_panda_token(panda_token, request: nil, validate: true, enforce_tenant: true) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'app/models/panda_pal/session.rb', line 21 def self.for_panda_token(panda_token, request: nil, validate: true, enforce_tenant: true) return nil unless panda_token.present? # <B64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: TYPE, exp?: EXPIRE, usig?: URL SIGNATURE, nnc?: NONCE })>.<HMAC(<B64HEADER>) | KEY> # TYPE = KEY | T-URI | T-IP | T-NONCE | T-EXP found_session = nil header, sig = panda_token.split('.') decoded_header = JSON.parse(Base64.urlsafe_decode64(header)) org_id = decoded_header['o'].to_i session_id = decoded_header['s'] type = decoded_header['typ'] tenant = "public" if org_id > 0 org = PandaPal::Organization.find(org_id) tenant = org.tenant_name end Apartment::Tenant.switch!(tenant) if enforce_tenant == :switch if enforce_tenant && Apartment::Tenant.current != tenant raise SessionNonceMismatch, "Session Not Found" end Apartment::Tenant.switch(tenant) do if type == 'KEY' found_session = PandaPal::Session.find_by(session_secret: sig) return found_session unless validate raise SessionNonceMismatch, "Session Not Found" unless found_session.present? raise SessionNonceMismatch, "Session Not Found" if session_id && found_session.id != session_id else session_record = PandaPal::Session.find_by(id: session_id) return session_record unless validate raise SessionNonceMismatch, "Session Not Found" unless session_record.present? # Validate the header against the signature raise SessionNonceMismatch, "Invalid Signature" unless session_record.validate_signature(header, sig) # Check expiration if (expr = decoded_header['exp']).present? raise SessionNonceMismatch, "Expired" unless expr > DateTime.now.iso8601 end if type == "T-URI" || type == "T-URL" # Signed URLs only support GET requests raise SessionNonceMismatch, "Invalid Method" unless request.method.to_s.upcase == "GET" # Verify the signature against the request URL # The usig doesn't _need_ to be signed (since it's part of the payload that is already signed), # but this was an easy way to make the value a constant length. Any basic hashing function could solve this, # but the HMAC implementation was already available. to_sign = format_url_for_signing(request.url) raise SessionNonceMismatch, "Invalid Signature" unless session_record.validate_signature(to_sign, decoded_header['usig']) # TODO Support single-use tokens via Redis? found_session = session_record elsif type == "T-NONCE" raise SessionNonceMismatch, "Invalid Nonce" unless session_record.data[:link_nonce] == decoded_header['nnc'] found_session = session_record found_session.data[:link_nonce] = nil elsif type == "T-IP" raise SessionNonceMismatch, "Invalid IP" unless decoded_header["ip"] == request.remote_ip found_session = session_record elsif type == "T-EXP" # Expiration itself is checked above, but enforce that an expiration is set raise SessionNonceMismatch, "Invalid Expiration" unless decoded_header["exp"].present? found_session = session_record else raise SessionNonceMismatch, "Invalid Panda Token Type" end end end raise SessionNonceMismatch, "Session Not Found" unless found_session.present? found_session end |
.for_request(request, params = request.params, enforce_tenant: true) ⇒ Object
14 15 16 17 18 19 |
# File 'app/models/panda_pal/session.rb', line 14 def self.for_request(request, params = request.params, enforce_tenant: true) return request.instance_variable_get(:@panda_pal_session) if request.instance_variable_defined?(:@panda_pal_session) panda_token = extract_panda_token(request, params) request.instance_variable_set(:@panda_pal_session, for_panda_token(panda_token, request: request, enforce_tenant: enforce_tenant)) end |
.format_url_for_signing(uri) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 |
# File 'app/models/panda_pal/session.rb', line 125 def self.format_url_for_signing(uri) uri = URI.parse(uri) if uri.is_a?(String) query = Rack::Utils.parse_query(uri.query) query.delete("panda_token") query.delete("session_key") query.delete("session_token") uri.query = Rack::Utils.build_query(query) "#{uri.path}?#{uri.query}" end |
Instance Method Details
#[](key) ⇒ Object
162 163 164 165 166 167 168 |
# File 'app/models/panda_pal/session.rb', line 162 def [](key) if self.class.column_names.include?(key.to_s) super else data[key] end end |
#[]=(key, value) ⇒ Object
170 171 172 173 174 175 176 |
# File 'app/models/panda_pal/session.rb', line 170 def []=(key, value) if self.class.column_names.include?(key.to_s) super else data[key] = value end end |
#cache(key, &blk) ⇒ Object
178 179 180 181 182 183 |
# File 'app/models/panda_pal/session.rb', line 178 def cache(key, &blk) data[:cache] ||= {} return data[:cache][key] if data[:cache].key?(key) data[:cache][key] = blk.call end |
#canvas_account_role_labels(account = 'self') ⇒ Object
Retrieve the User’s Role Labels in the specified Account, defaulting to the Root Account
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'app/models/panda_pal/session.rb', line 242 def canvas_account_role_labels(account = 'self') account = 'self' if account.to_s == "root" account = account.canvas_id if account.respond_to?(:canvas_id) if defined?(::Admin) && ::Admin < ::ActiveRecord::Base account = current_organization.canvas_account_id if account == 'self' adm_query = ::Admin.where(canvas_account_id: account, workflow_state: "active") adm_query.pluck(:role_name) else Rails.cache.fetch([self.class.name, "AccountAdminLinks", account, canvas_user_id], expires_in: 1.hour) do admin_entries = canvas_sync_client.account_admins(account, user_id: [canvas_user_id]) admin_entries = admin_entries.select{|ent| ent[:workflow_state] == 'active' } admin_entries.map{|ent| ent[:role] } end end end |
#canvas_role_labels ⇒ Object
Retrieve the User’s Role Labels in the Launch Context
236 237 238 239 |
# File 'app/models/panda_pal/session.rb', line 236 def canvas_role_labels labels = get_lti_cust_param('custom_canvas_role') labels.is_a?(String) ? labels.split(',') : [] end |
#canvas_site_admin? ⇒ Boolean
273 274 275 |
# File 'app/models/panda_pal/session.rb', line 273 def canvas_site_admin? lti_roles.system_roles.include?("sys_admin") end |
#canvas_user_id ⇒ Object
263 264 265 |
# File 'app/models/panda_pal/session.rb', line 263 def canvas_user_id get_lti_cust_param('canvas_user_id') end |
#custom_lti_params ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'app/models/panda_pal/session.rb', line 199 def custom_lti_params @custom_lti_params ||= begin # LT 1.3 custom_params = launch_params[PandaPal::LtiConstants::Claims::CUSTOM] return custom_params if custom_params.present? # LTI 1.0/1.1 custom_params = {} launch_params.each do |k, v| next unless k.start_with?("custom_") custom_params[k[7..-1]] = v end custom_params.with_indifferent_access end end |
#get_lti_cust_param(key, default: :if_not_var) ⇒ Object
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'app/models/panda_pal/session.rb', line 216 def get_lti_cust_param(key, default: :if_not_var) nkey = key.to_s.gsub(/^custom_/, '') default_value = ->() { PandaPal.lti_custom_params[nkey] || PandaPal.lti_custom_params["custom_#{nkey}"] } val = launch_params.dig("https://purl.imsglobal.org/spec/lti/claim/custom", nkey) || launch_params[nkey] || launch_params["custom_#{nkey}"] if default == :if_not_var if val.is_a?(String) && /\$[\.\w]+/.match?(val) && val == default_value[] return nil end elsif default && !val.present? return default_value[] elsif !default && val == default_value[] return nil end val end |
#launch_params ⇒ Object
185 186 187 |
# File 'app/models/panda_pal/session.rb', line 185 def launch_params data[:launch_params] || {} end |
#lti_launch_placement ⇒ Object
195 196 197 |
# File 'app/models/panda_pal/session.rb', line 195 def lti_launch_placement launch_params['https://www.instructure.com/placement'] || launch_params[:launch_type] end |
#lti_platform ⇒ Object
189 190 191 192 193 |
# File 'app/models/panda_pal/session.rb', line 189 def lti_platform return nil unless data[:lti_platform].present? @lti_platform ||= Platform.from_serialized(data[:lti_platform]) end |
#lti_roles ⇒ Object
259 260 261 |
# File 'app/models/panda_pal/session.rb', line 259 def lti_roles @lti_roles ||= RoleStore.new(launch_params["https://purl.imsglobal.org/spec/lti/claim/roles"] || launch_params['ext_roles'] || '') end |
#session_key ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 |
# File 'app/models/panda_pal/session.rb', line 137 def session_key # <B64({ v: TOKEN FORMAT, o: ORG ID, s: SESSION ID, typ: "KEY" })>.<SESSION SECRET> payload = { v: 1, o: panda_pal_organization_id, s: id, typ: "KEY", } base64_payload = Base64.urlsafe_encode64(payload.to_json) "#{base64_payload}.#{session_secret}" end |
#sign_value(data) ⇒ Object
153 154 155 |
# File 'app/models/panda_pal/session.rb', line 153 def sign_value(data) OpenSSL::HMAC.base64digest(OpenSSL::Digest.new('sha256'), signing_key, data) end |
#signing_key ⇒ Object
149 150 151 |
# File 'app/models/panda_pal/session.rb', line 149 def signing_key session_secret end |
#user ⇒ Object
267 268 269 270 271 |
# File 'app/models/panda_pal/session.rb', line 267 def user return nil unless canvas_user_id.present? @user ||= ::User.find_by(canvas_id: canvas_user_id) if defined?(::User) && ::User < ::ActiveRecord::Base end |
#validate_signature(data, signature) ⇒ Object
157 158 159 160 |
# File 'app/models/panda_pal/session.rb', line 157 def validate_signature(data, signature) # TODO Constant time signature == sign_value(data) end |