dce_lti 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93969389a33a1c7eabee7af327fd8f3b38df303e
4
- data.tar.gz: 9a042ba6f70ac543103a9c66087829f6db3cf672
3
+ metadata.gz: a4dcee8b8e745797874479ffb1d75eb6e468bd16
4
+ data.tar.gz: 844b534a13367e71716209aaf8818a74ff19c9aa
5
5
  SHA512:
6
- metadata.gz: e71bb0b7f2f05bc3f360f194857efa660dbf37a1e06fd14ad2b48198317ed64a0877fd696f6aa7151137a6074f027919c1ee0dcc204539aef550ce0632ecdf94
7
- data.tar.gz: e43d735ebe797fde5a6f78b0f5e81c8390bedba4df62c667fb44632e2cd732dd2ad638adc4ef5c6d5ebd58c66ded71bc48d155086f37e9f1a21813ed305f574b
6
+ metadata.gz: edebe996ebdddee116b497723a5958fd494027adfc4292e36eca9880737d84d6bb8ab12da1615be1605be6445fbc66a4b5737d6b2c4521d232b012c197ac5ddd
7
+ data.tar.gz: 60aec8cf3c206bf68891a781a125f8c5d66348dc734989a88a5d1e9a4d69b27152321e57ba9f1b92957ae87c3c0a92532faf62ed971ebdfd7fb05f4b21351046
data/README.md CHANGED
@@ -10,11 +10,18 @@ the [IMS::LTI gem](https://github.com/instructure/ims-lti).
10
10
 
11
11
  ## Getting started
12
12
 
13
- Add this engine to your gemfile:
13
+ Add these gems to your gemfile:
14
14
 
15
15
  gem 'dce_lti'
16
+ gem 'activerecord-session_store', '~> 0.1.1''
16
17
 
17
- Install it and run migrations:
18
+ Update (or create) `config/initializers/session_store.rb` and ensure it contains:
19
+
20
+ Rails.application.config.session_store :active_record_store, key: '_your_app_session', expire_after: 60.minutes
21
+
22
+ Where `_your_app_session` is your application's session key.
23
+
24
+ Bundle, install and then run migrations:
18
25
 
19
26
  bundle
20
27
  rake dce_lti:install
@@ -36,6 +43,10 @@ you have a valid LTI-provided user in `current_user`, thusly:
36
43
  end
37
44
  end
38
45
 
46
+ That's it! You'll need to configure to fit your use case, but you've got the
47
+ basics of LTI authentication (including experimental cookieless sessioning, see
48
+ below) working already.
49
+
39
50
  ## Configuration
40
51
 
41
52
  The generated config looks something like (commented defaults omitted):
@@ -105,7 +116,7 @@ See
105
116
  and other classes/modules under the IMS::LTI::Extensions hierarchy for further
106
117
  options.
107
118
 
108
- ## Notes
119
+ ## Other
109
120
 
110
121
  The `DceLti` provided controllers inherit from `ApplicationController` as
111
122
  defined in your application.
@@ -138,6 +149,95 @@ rendered. You can customize this output by creating a file named
138
149
  `app/views/dce_lti/sessions/invalid.html.erb`, per the default engine view
139
150
  resolution behavior.
140
151
 
152
+ ### Cookieless Sessions - Experimental
153
+
154
+ If you're running your LTI app on a domain different than your LMS, it will not
155
+ work in recent Safari browers. This is because [Safari blocks third party
156
+ cookies set in an iframe by
157
+ default](https://support.apple.com/kb/PH19214?locale=en_US). Mozilla has hinted
158
+ at implementing this default as well, so the days of setting a cookie in an
159
+ iframe and expecting it to work are probably numbered. Thanks, pervasive ad
160
+ networks!
161
+
162
+ There are a few options:
163
+
164
+ 1. Run your LMS and LTI provider on the same domain. This isn't really doable
165
+ if you want to provide a tool useful to multiple consumers on multiple domains,
166
+ 1. Only provide completely anonymous LTI tools,
167
+ 1. Build single page javascript-driven apps,
168
+ 1. Ask your users to enable third-party cookies,
169
+ 1. Block users when you detect they don't support third-party cookies,
170
+ 1. Persist the users session by including it in every link and form.
171
+
172
+ This engine implements the last option by detecting when a browser doesn't
173
+ accept cookies and then rewriting outgoing URLs and forms to include a session.
174
+
175
+ This behavior is disabled by default and requires minimal app-level changes:
176
+
177
+ 1. Edit `config/initializers/dce_lti_config.rb`. Uncomment
178
+ `lti.enable_cookieless_sessions = false` and set it to `true`.
179
+ 1. You must use database sessions as provided by `activerecord-session_store`,
180
+ which we install by default and you should've already configured.
181
+ 1. The `redirect_after_successful_auth` path must include the session key and
182
+ id so we can pick it up if cookies aren't available (this is the default as
183
+ well).
184
+
185
+ Please report bugs to github issues, there are bound to be a few.
186
+
187
+ If a user supports cookies, we do basically nothing. We don't rewrite forms or
188
+ URLs and we use a cookied (and database-backed) session per the usual.
189
+
190
+ #### How cookieless sessions work
191
+
192
+ When a request comes in without a cookie but with the session key and ID, then
193
+ the `DceLti::Middleware::CookieShim` middleware "shims" it into the Rack
194
+ environment and the session information is restored by subsequent middleware.
195
+
196
+ When we detect that a user doesn't accept third-party cookies, we use
197
+ `Rack::Plastic` to rewrite forms and URLs to include the session key and id
198
+ from the `redirect_after_successful_auth` redirect. This happens in the
199
+ `DceLti::Middleware::CookielessSessions` middleware.
200
+
201
+ #### Known issues with cookieless sessions
202
+
203
+ * Even if your app works with cookieless sessions, other cookie sessioned
204
+ iframe'd apps won't: for instance the youtube javascript iframe API and many
205
+ other third-party javascript apps.
206
+ * We only rewrite URLs without a protocol and domain ('/posts/1') to match the
207
+ URLs emitted by rails by default. If you're manually inserting links to your
208
+ application that include the protocol and domain name
209
+ ('http://example.com/posts/1'), the middleware doesn't catch it. This could
210
+ be fixed to be a bit smarter in the future.
211
+ * You will need to ensure that the session key and id tags along for ajax
212
+ requests to your LTI application.
213
+
214
+ #### Database session cleanup
215
+
216
+ Run the included `dce_lti:clean_sessions` rake task periodically to remove old
217
+ sessions - the default is 7 days, you can modify this with the
218
+ `OLDER_THAN_X_DAYS` environment variable, thusly:
219
+
220
+ OLDER_THAN_X_DAYS=14 rake dce_lti:clean_sessions
221
+
222
+ #### Database session hijacking for cookieless sessions
223
+
224
+ This is an issue, unfortunately. If a malicious user were able to get ahold of
225
+ a link in another user's LTI session (when that other user is under a
226
+ cookieless session) it'd contain a working session ID and could be exploited.
227
+
228
+ This can be mitigated several ways:
229
+
230
+ 1. Deliver your LTI application over SSL to protect the transport layer. You
231
+ pretty much need to do this anyway, so this shouldn't be a big deal.
232
+ 1. Expire your sessions by setting the `expire_after` option in
233
+ `config/initializers/session_store.rb` to a value short enough to not annoy
234
+ your users.
235
+
236
+ If you set `expire_after` too short, your users will get annoyed. If you set it
237
+ too long, the sessions will linger and increase the time the session is
238
+ vulnerable. We're looking into other ways of mitigating this as well - PRs
239
+ accepted!
240
+
141
241
  ### Nonce cleanup
142
242
 
143
243
  You can clean up lti-related
@@ -0,0 +1,12 @@
1
+ class AddSessionsTable < ActiveRecord::Migration
2
+ def change
3
+ create_table :sessions do |t|
4
+ t.string :session_id, :null => false
5
+ t.text :data
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :sessions, :session_id, :unique => true
10
+ add_index :sessions, :updated_at
11
+ end
12
+ end
@@ -14,5 +14,10 @@ module DceLti
14
14
  User.find_by(id: session[:current_user_id])
15
15
  end
16
16
  end
17
+
18
+ def cookieless_session?
19
+ cookie = env.fetch('HTTP_COOKIE', '')
20
+ cookie.blank? || cookie.match(/shimmed_cookie/)
21
+ end
17
22
  end
18
23
  end
@@ -12,20 +12,34 @@ resource_link_id
12
12
  resource_link_title
13
13
  tool_consumer_instance_guid
14
14
  launch_presentation_return_url
15
- |
15
+ |
16
+
17
+ config.enable_cookieless_sessions = false
16
18
 
17
19
  config.provider_title = (ENV['LTI_PROVIDER_TITLE'] || 'DCE LTI Provider')
18
20
  config.provider_description = (ENV['LTI_PROVIDER_DESCRIPTION'] || 'A description of this')
19
21
 
20
22
  config.redirect_after_successful_auth = -> (controller) do
21
- Rails.application.routes.url_helpers.root_path
23
+ session_key_name = Rails.application.config.session_options[:key]
24
+ Rails.application.routes.url_helpers.root_path(session_key_name => controller.session.id)
22
25
  end
26
+
23
27
  config.tool_config_extensions = ->(*) {}
24
28
  yield config
25
29
  end
26
30
 
27
31
  initializer 'dce_lti.load_helpers' do
28
32
  ActionController::Base.send :include, ControllerMethods
33
+ ActionController::Base.send :include, RedirectToHelper
34
+ ActionController::Base.send :helper, RedirectToHelper
35
+ ApplicationController.skip_before_filter :verify_authenticity_token, if: :cookieless_session?
36
+ end
37
+
38
+ initializer 'dce_lti.add_middleware' do |app|
39
+ if config.enable_cookieless_sessions
40
+ app.middleware.insert_before ActionDispatch::Cookies, 'DceLti::Middleware::CookieShim'
41
+ app.middleware.use 'DceLti::Middleware::CookielessSessions'
42
+ end
29
43
  end
30
44
 
31
45
  isolate_namespace DceLti
@@ -0,0 +1,36 @@
1
+ module DceLti
2
+ module Middleware
3
+ class CookieShim
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env['HTTP_COOKIE'].to_s.strip.empty?
10
+ params = parse_query_string(env)
11
+ if params[session_key_name.to_s]
12
+ env['HTTP_COOKIE'] = "#{session_key_name}=#{params[session_key_name.to_s]};shimmed_cookie=1"
13
+ end
14
+ end
15
+
16
+ @app.call(env)
17
+ end
18
+
19
+ private
20
+
21
+ def parse_query_string(env)
22
+ query_string = env['QUERY_STRING']
23
+ params = {}
24
+ query_string.split('&').each do |parameter|
25
+ (key, value) = parameter.split('=')
26
+ params[key] = value
27
+ end
28
+ params
29
+ end
30
+
31
+ def session_key_name
32
+ @session_key_name ||= Rails.application.config.session_options[:key]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ require 'rack-plastic'
2
+
3
+ module DceLti
4
+ module Middleware
5
+ class CookielessSessions < Rack::Plastic
6
+ def change_nokogiri_doc(doc)
7
+ if no_cookies? || shimmed_cookie?
8
+ doc.css('a').each do |a|
9
+ href = a[:href]
10
+
11
+ next unless local_url?(href)
12
+ next if url_has_key_already?(href)
13
+
14
+ if href.match(/\?/)
15
+ a[:href] += "&#{session_key_name}=#{session_id}"
16
+ else
17
+ a[:href] += "?#{session_key_name}=#{session_id}"
18
+ end
19
+ end
20
+
21
+ doc.css('form').each do |form|
22
+ action = form[:action]
23
+ next unless local_url?(action)
24
+ next if url_has_key_already?(action)
25
+
26
+ # For PATCH, PUT, DELETE and POST, which allow
27
+ # params mixed in the action and the form.
28
+ if action.match(/\?/)
29
+ form[:action] += "&#{session_key_name}=#{session_id}"
30
+ else
31
+ form[:action] += "?#{session_key_name}=#{session_id}"
32
+ end
33
+
34
+ # For GET, oddly. GET method forms stomp all params encoded
35
+ # in the action
36
+ input_node = Nokogiri::XML::Node.new('input', doc)
37
+ input_node[:type] = 'hidden'
38
+ input_node[:name] = session_key_name
39
+ input_node[:value] = session_id
40
+ form.children.first.add_previous_sibling(
41
+ input_node
42
+ )
43
+ end
44
+ end
45
+ doc
46
+ end
47
+
48
+ private
49
+
50
+ def shimmed_cookie?
51
+ @p.request.env['HTTP_COOKIE'].to_s.strip.match(/shimmed_cookie/)
52
+ end
53
+
54
+ def no_cookies?
55
+ @p.request.env['HTTP_COOKIE'].to_s.strip.empty?
56
+ end
57
+
58
+ def local_url?(url)
59
+ ! url.match(/\Ahttps?:\/\/|\/\//i)
60
+ end
61
+
62
+ def url_has_key_already?(url)
63
+ url.match(/#{session_key_name}/i)
64
+ end
65
+
66
+ def session_key_name
67
+ @session_key_name ||= Rails.application.config.session_options[:key]
68
+ end
69
+
70
+ def session
71
+ @p.request.env['rack.session']
72
+ end
73
+
74
+ def session_id
75
+ session.id
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,25 @@
1
+ module DceLti
2
+ module RedirectToHelper
3
+ def redirect_to(options)
4
+ session_key_name = Rails.application.config.session_options[:key]
5
+ if request.env.fetch('HTTP_COOKIE', '').match(/shimmed_cookie/) &&
6
+ (::DceLti::Engine.config.enable_cookieless_sessions)
7
+ case options
8
+ when Hash
9
+ options.merge!(session_key_name => session.id)
10
+ when String
11
+ if options.match(/\?/)
12
+ unless options.match(/#{session_key_name}/)
13
+ options += %Q|&#{session_key_name}=#{session.id}|
14
+ end
15
+ else
16
+ unless options.match(/#{session_key_name}/)
17
+ options += %Q|?#{session_key_name}=#{session.id}|
18
+ end
19
+ end
20
+ end
21
+ end
22
+ super(options)
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module DceLti
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/dce_lti.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require "dce_lti/engine"
2
2
  require 'dce_lti/controller_methods'
3
+ require 'dce_lti/redirect_to_helper'
4
+ require 'dce_lti/middleware/cookie_shim'
5
+ require 'dce_lti/middleware/cookieless_sessions'
3
6
 
4
7
  module DceLti
5
8
  end
@@ -31,4 +31,11 @@ engine provides.
31
31
  task clean_nonces: :environment do
32
32
  DceLti::Nonce.clean
33
33
  end
34
+
35
+ desc 'Clean up old sessions'
36
+ task clean_sessions: :environment do
37
+ older_than = (ENV.fetch('OLDER_THAN_X_DAYS', 7)).to_i
38
+ session_klass = ActionDispatch::Session::ActiveRecordStore.session_class
39
+ session_klass.where('updated_at < ?', (Time.now - older_than.days)).delete_all
40
+ end
34
41
  end
@@ -5,7 +5,21 @@ DceLti::Engine.setup do |lti|
5
5
  #
6
6
  # lti.provider_title = (ENV['LTI_PROVIDER_TITLE'] || 'DCE LTI Provider')
7
7
  # lti.provider_description = (ENV['LTI_PROVIDER_DESCRIPTION'] || 'A description of this')
8
- # lti.redirect_after_successful_auth = ->(controller) { Rails.application.routes.url_helpers.root_path }
8
+ #
9
+ # Set this to `true` to enable the form and URL-rewriting behavior that
10
+ # allows for the creation of cookieless sessions. The default is `false`,
11
+ # meaning we don't attempt to use cookieless sessions when a cookie cannot be
12
+ # set - the session just fails.
13
+ #
14
+ # lti.enable_cookieless_sessions = false
15
+ #
16
+ # The default post-auth redirect includes the session key and session id so
17
+ # that we can instantiate a successful cookieless session if needed.
18
+ #
19
+ # lti.redirect_after_successful_auth = ->(controller) {
20
+ # session_key_name = Rails.application.config.session_options[:key]
21
+ # Rails.application.routes.url_helpers.root_path(session_key_name => controller.session.id)
22
+ # }
9
23
 
10
24
  lti.consumer_secret = (ENV['LTI_CONSUMER_SECRET'] || 'consumer_secret')
11
25
  lti.consumer_key = (ENV['LTI_CONSUMER_KEY'] || 'consumer_key')
@@ -2983,3 +2983,69 @@ Completed 200 OK in 3ms (Views: 0.4ms | ActiveRecord: 0.6ms)
2983
2983
   (0.2ms) SELECT version FROM "schema_migrations"
2984
2984
   (1.2ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
2985
2985
   (1.1ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
2986
+  (105.5ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
2987
+  (241.9ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
2988
+ SQL (0.1ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
2989
+  (13.1ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
2990
+  (4.4ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
2991
+  (6.6ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
2992
+  (1.4ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
2993
+  (3.4ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
2994
+  (0.3ms) SELECT version FROM "schema_migrations"
2995
+  (1.2ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
2996
+  (1.0ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
2997
+  (205.8ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
2998
+  (227.7ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
2999
+ SQL (0.1ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
3000
+  (7.5ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
3001
+  (3.5ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
3002
+  (13.6ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
3003
+  (2.7ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
3004
+  (7.0ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
3005
+  (0.2ms) SELECT version FROM "schema_migrations"
3006
+  (2.3ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
3007
+  (2.2ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
3008
+  (104.9ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
3009
+  (233.8ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
3010
+ SQL (0.2ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
3011
+  (8.3ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
3012
+  (3.3ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
3013
+  (7.5ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
3014
+  (1.5ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
3015
+  (7.3ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
3016
+  (0.5ms) SELECT version FROM "schema_migrations"
3017
+  (2.4ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
3018
+  (2.6ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
3019
+  (205.3ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
3020
+  (229.4ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
3021
+ SQL (0.2ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
3022
+  (16.2ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
3023
+  (4.8ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
3024
+  (7.7ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
3025
+  (1.3ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
3026
+  (3.3ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
3027
+  (0.3ms) SELECT version FROM "schema_migrations"
3028
+  (1.0ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
3029
+  (1.0ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
3030
+  (107.1ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
3031
+  (329.4ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
3032
+ SQL (0.2ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
3033
+  (15.0ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
3034
+  (7.1ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
3035
+  (15.3ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
3036
+  (2.6ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
3037
+  (7.6ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
3038
+  (0.3ms) SELECT version FROM "schema_migrations"
3039
+  (2.6ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
3040
+  (2.3ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')
3041
+  (106.1ms) DROP DATABASE IF EXISTS "dce_lti_dummy_test"
3042
+  (233.4ms) CREATE DATABASE "dce_lti_dummy_test" ENCODING = 'utf8'
3043
+ SQL (0.4ms) CREATE EXTENSION IF NOT EXISTS "plpgsql"
3044
+  (10.2ms) CREATE TABLE "dce_lti_nonces" ("id" serial primary key, "nonce" character varying(255), "created_at" timestamp, "updated_at" timestamp)
3045
+  (7.4ms) CREATE UNIQUE INDEX "index_dce_lti_nonces_on_nonce" ON "dce_lti_nonces" USING btree ("nonce")
3046
+  (15.2ms) CREATE TABLE "dce_lti_users" ("id" serial primary key, "lti_user_id" character varying(255), "lis_person_contact_email_primary" character varying(255), "lis_person_name_family" character varying(255), "lis_person_name_full" character varying(255), "lis_person_name_given" character varying(255), "lis_person_sourcedid" character varying(255), "user_image" character varying(255), "roles" character varying(255)[] DEFAULT '{}', "created_at" timestamp, "updated_at" timestamp)
3047
+  (2.6ms) CREATE TABLE "schema_migrations" ("version" character varying(255) NOT NULL) 
3048
+  (6.8ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
3049
+  (0.2ms) SELECT version FROM "schema_migrations"
3050
+  (2.1ms) INSERT INTO "schema_migrations" (version) VALUES ('20141008172001')
3051
+  (2.1ms) INSERT INTO "schema_migrations" (version) VALUES ('20141003180140')