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
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'app/models/panda_pal/session.rb', line 107 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 105 |
# 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? expr = Time.parse(expr).utc raise SessionNonceMismatch, "Expired" unless expr > DateTime.now.utc 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
126 127 128 129 130 131 132 133 134 135 136 |
# File 'app/models/panda_pal/session.rb', line 126 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
163 164 165 166 167 168 169 |
# File 'app/models/panda_pal/session.rb', line 163 def [](key) if self.class.column_names.include?(key.to_s) super else data[key] end end |
#[]=(key, value) ⇒ Object
171 172 173 174 175 176 177 |
# File 'app/models/panda_pal/session.rb', line 171 def []=(key, value) if self.class.column_names.include?(key.to_s) super else data[key] = value end end |
#cache(key, &blk) ⇒ Object
179 180 181 182 183 184 |
# File 'app/models/panda_pal/session.rb', line 179 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
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'app/models/panda_pal/session.rb', line 243 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", canvas_user_id: canvas_user_id) 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
237 238 239 240 |
# File 'app/models/panda_pal/session.rb', line 237 def canvas_role_labels labels = get_lti_cust_param('custom_canvas_role') labels.is_a?(String) ? labels.split(',') : [] end |
#canvas_site_admin? ⇒ Boolean
274 275 276 |
# File 'app/models/panda_pal/session.rb', line 274 def canvas_site_admin? lti_roles.system_roles.include?("sys_admin") end |
#canvas_user_id ⇒ Object
264 265 266 |
# File 'app/models/panda_pal/session.rb', line 264 def canvas_user_id get_lti_cust_param('canvas_user_id') end |
#custom_lti_params ⇒ Object
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'app/models/panda_pal/session.rb', line 200 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
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'app/models/panda_pal/session.rb', line 217 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
186 187 188 |
# File 'app/models/panda_pal/session.rb', line 186 def launch_params data[:launch_params] || {} end |
#lti_launch_placement ⇒ Object
196 197 198 |
# File 'app/models/panda_pal/session.rb', line 196 def lti_launch_placement launch_params['https://www.instructure.com/placement'] || launch_params[:launch_type] end |
#lti_platform ⇒ Object
190 191 192 193 194 |
# File 'app/models/panda_pal/session.rb', line 190 def lti_platform return nil unless data[:lti_platform].present? @lti_platform ||= Platform.from_serialized(data[:lti_platform]) end |
#lti_roles ⇒ Object
260 261 262 |
# File 'app/models/panda_pal/session.rb', line 260 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
138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/models/panda_pal/session.rb', line 138 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
154 155 156 |
# File 'app/models/panda_pal/session.rb', line 154 def sign_value(data) OpenSSL::HMAC.base64digest(OpenSSL::Digest.new('sha256'), signing_key, data) end |
#signing_key ⇒ Object
150 151 152 |
# File 'app/models/panda_pal/session.rb', line 150 def signing_key session_secret end |
#user ⇒ Object
268 269 270 271 272 |
# File 'app/models/panda_pal/session.rb', line 268 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
158 159 160 161 |
# File 'app/models/panda_pal/session.rb', line 158 def validate_signature(data, signature) # TODO Constant time signature == sign_value(data) end |