├── docs ├── home.png ├── lti-url.png ├── edx-signin.png ├── lti-setup.png ├── custom-params.png ├── launch-button.png ├── login-dialog.png ├── lti-component.png ├── edx-lti-launch.png ├── open-new-window.png └── signedin-forum.png ├── config ├── locales │ └── server.en.yml └── settings.yml ├── LICENSE ├── .gitignore ├── plugin.rb ├── lti_authenticator.rb ├── README.md └── lti_strategy.rb /docs/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/home.png -------------------------------------------------------------------------------- /docs/lti-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/lti-url.png -------------------------------------------------------------------------------- /docs/edx-signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/edx-signin.png -------------------------------------------------------------------------------- /docs/lti-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/lti-setup.png -------------------------------------------------------------------------------- /docs/custom-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/custom-params.png -------------------------------------------------------------------------------- /docs/launch-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/launch-button.png -------------------------------------------------------------------------------- /docs/login-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/login-dialog.png -------------------------------------------------------------------------------- /docs/lti-component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/lti-component.png -------------------------------------------------------------------------------- /docs/edx-lti-launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/edx-lti-launch.png -------------------------------------------------------------------------------- /docs/open-new-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/open-new-window.png -------------------------------------------------------------------------------- /docs/signedin-forum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-teaching-systems-lab/discourse-edx-lti/HEAD/docs/signedin-forum.png -------------------------------------------------------------------------------- /config/locales/server.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | site_settings: 3 | lti_consumer_key: "LTI consumer key" 4 | lti_consumer_secret: "LTI consumer secret" 5 | lti_consumer_authenticate_url: 'EdX authentication URL (eg., course page with link to forums)' 6 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | lti_consumer_key: 3 | default: 'foo' 4 | client: false 5 | lti_consumer_secret: 6 | default: 'bar' 7 | client: false 8 | lti_consumer_authenticate_url: 9 | default: 'https://edx.org/foo' 10 | client: true 11 | 12 | login: 13 | invite_only: true 14 | login_required: true 15 | must_approve_users: false 16 | enable_local_logins: true 17 | allow_new_registrations: true 18 | 19 | users: 20 | email_editable: false 21 | username_change_period: 0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | -------------------------------------------------------------------------------- /plugin.rb: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------- 2 | # name: discourse-edx-lti 3 | # about: Discourse plugin to authenticate with LTI (eg., for an EdX course) 4 | # version: 0.8.0 5 | # author: MIT Teaching Systems Lab 6 | # url: https://github.com/mit-teaching-systems-lab/discourse-edx-lti 7 | # required_version: 1.9.0.beta8 8 | # --------------------------------------------------------------- 9 | 10 | # Plugins need to explicitly include their dependencies, and the loading 11 | # mechanism is different than with bundler. 12 | # See https://github.com/discourse/discourse/blob/master/lib/plugin_gem.rb 13 | gem 'ims-lti', '1.1.13', require: false, require_name: 'ims/lti' 14 | 15 | 16 | # Enable site settings in admin UI. 17 | # See descriptions in files in the `config` folder. 18 | enabled_site_setting :lti_consumer_key 19 | enabled_site_setting :lti_consumer_secret 20 | enabled_site_setting :lti_consumer_authenticate_url 21 | 22 | 23 | # Register Discourse AuthProvider 24 | require_relative 'lti_strategy.rb' 25 | require_relative 'lti_authenticator.rb' 26 | auth_provider({ 27 | title: 'Click to login with EdX', 28 | message: 'Click to login with EdX', 29 | authenticator: LTIAuthenticator.new, 30 | full_screen_login: true, 31 | custom_url: '/lti/redirect_to_consumer' 32 | }) 33 | 34 | 35 | # This styles the login button, and overrides #login-form to 36 | # adds a little more separation between the EdX login button and 37 | # the the normal login form below (which is only for admin users). 38 | register_css < 'lti#redirect_to_consumer' 80 | end 81 | Discourse::Application.routes.append do 82 | mount ::DiscourseEdxLti::Engine, at: '/lti' 83 | end 84 | 85 | require_dependency 'application_controller' 86 | class ::DiscourseEdxLti::LtiController < ::ApplicationController 87 | requires_plugin PLUGIN_NAME 88 | 89 | # Adapted from Discourse's StaticController#enter 90 | skip_before_action :check_xhr, :redirect_to_login_if_required, :verify_authenticity_token 91 | 92 | def redirect_to_consumer 93 | url = SiteSetting.lti_consumer_authenticate_url 94 | redirect_to url 95 | end 96 | end 97 | end -------------------------------------------------------------------------------- /lti_authenticator.rb: -------------------------------------------------------------------------------- 1 | # Discourse Authenticator class 2 | class LTIAuthenticator < ::Auth::Authenticator 3 | DISCOURSE_USERNAME_MAX_LENGTH = 20 4 | 5 | # override hook 6 | def name 7 | 'lti' 8 | end 9 | 10 | # override hook 11 | def register_middleware(omniauth) 12 | log :info, 'register_middleware' 13 | omniauth.provider :lti 14 | end 15 | 16 | # override hook 17 | # The UX we want here is that if this is the first time a learner has authenticated, 18 | # we'll create a new user record for them automatically (so they don't see the modal 19 | # for creating their own user). A second-time learner should just be authenticated 20 | # and go right into Discourse. 21 | # 22 | # We've set `SiteSetting.invite_only?` to true in order to disable the "Sign up" flow 23 | # in Discourse. So this code instantiates a new User record because otherwise the 24 | # standard flow will popup a dialog to let them change their username, and that would 25 | # fail to create a new user since `SiteSetting.invite_only?` is true. 26 | def after_authenticate(auth_token) 27 | log :info, 'after_authenticate' 28 | log :info, "after_authenticate, auth_token: #{auth_token.inspect}" 29 | auth_result = Auth::Result.new 30 | 31 | # Grab the info we need from OmniAuth 32 | # We also may need to modify the EdX username to conform to Discourse's username 33 | # validations. 34 | omniauth_params = auth_token[:info] 35 | auth_result.username = build_discourse_username omniauth_params[:edx_username] 36 | auth_result.name = omniauth_params[:edx_username] 37 | auth_result.email = omniauth_params[:email] 38 | auth_result.email_valid = auth_result.email.present? 39 | lti_uid = auth_token[:uid] 40 | auth_result.extra_data = omniauth_params.merge(lti_uid: lti_uid) 41 | log :info, "after_authenticate, auth_result: #{auth_result.inspect}" 42 | 43 | # Lookup or create a new User record, requiring that both email and username match. 44 | # Discourse's User model patches some Rails methods, so we use their 45 | # methods here rather than reaching into details of how these fields are stored in the DB. 46 | # This appears related to changes in https://github.com/discourse/discourse/pull/4977 47 | user_by_email = User.find_by_email(auth_result.email.downcase) 48 | user_by_username = User.find_by_username(auth_result.username) 49 | both_matches_found = user_by_email.present? && user_by_username.present? 50 | no_matches_found = user_by_email.nil? && user_by_username.nil? 51 | if both_matches_found && user_by_email.id == user_by_username.id 52 | log :info, "after_authenticate, found user records by both username and email and they matched, using existing user..." 53 | user = user_by_email 54 | elsif no_matches_found 55 | log :info, "after_authenticate, no matches found for email or username, creating user record for first-time user..." 56 | user = User.new(email: auth_result.email.downcase, username: auth_result.username) 57 | user.staged = false 58 | user.active = true 59 | user.password = SecureRandom.hex(32) 60 | user.save! 61 | user.reload 62 | else 63 | log :info, "after_authenticate, found user records that did not match by username and email" 64 | log :info, "after_authenticate, user_by_email: #{user_by_email.inspect}" 65 | log :info, "after_authenticate, user_by_username: #{user_by_username.inspect}" 66 | raise ::ActiveRecord::RecordInvalid('LTIAuthenticator: edge case for finding User records where username and email did not match, aborting...') 67 | end 68 | 69 | # Return a reference to the User record. 70 | auth_result.user = user 71 | log :info, "after_authenticate, user: #{auth_result.user.inspect}" 72 | 73 | # This isn't needed for authentication, it just tracks the unique EdX user ids 74 | # in a way we could look them up from the EdX username if we needed to. 75 | plugin_store_key = "lti_username_#{auth_result.username}" 76 | ::PluginStore.set('lti', plugin_store_key, auth_result.as_json) 77 | log :info, "after_authenticate, PluginStore.set for auth_result: #{auth_result.as_json}" 78 | 79 | auth_result 80 | end 81 | 82 | protected 83 | def log(method_symbol, text) 84 | Rails.logger.send(method_symbol, "LTIAuthenticator: #{text}") 85 | end 86 | 87 | # Take valid EdX usernames that would be invalid Discourse usernames, and transform 88 | # them into valid Discourse usernames. 89 | # Right now this method just handles the cases we've run into in the wild - 90 | # Discourse usernames can't be too long, can't end on special symbol (_) and 91 | # can't contain more than 1 underscore in a row. 92 | # See https://github.com/discourse/discourse/blob/v1.9.0.beta17/app/models/username_validator.rb#L29 for 93 | # full details on Discourse validation. 94 | # 95 | # This method can lead to collapsing different EdX usernames into the same Discourse 96 | # username (eg, kevin__robinson and kevin_robinson), but the authentication methods above 97 | # require that email addresses match exactly as well. 98 | def build_discourse_username(edx_username) 99 | edx_username.slice(0, DISCOURSE_USERNAME_MAX_LENGTH).gsub('__','_').chomp('_') 100 | end 101 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discourse-edx-lti 2 | This is a [Discourse](http://www.discourse.org/) plugin for using Discourse as a discussion forum in EdX courses. 3 | 4 | It adds a Discourse `AuthProvider` that handles LTI authentication from EdX. This allows one-click navigation from an EdX course into the discussion forum. The login dialog also allows users to bounce back to the course, and Discourse admin users can sign in directly. 5 | 6 | Alternately, you may be interested in a similar project using [WordPress](https://github.com/mit-teaching-systems-lab/wordpress-edx-forums) as a discussion forum for EdX courses. 7 | 8 | ## Where has this been used? 9 | - MIT: [Design Thinking for Leading and Learning](https://www.edx.org/course/design-thinking-leading-learning-mitx-microsoft-education-11-155x) ([course report](https://tsl.mit.edu/wp-content/uploads/2017/06/DTLL_Review_2017.pdf)) 10 | - MIT: [Launching Innovation in Schools](https://courses.edx.org/courses/course-v1:MITx+11.154x+3T2017/course/), also on EdX Edge for a white-labeled small private online course 11 | 12 | ## Learner user experience 13 | #### 1. Learner signs into EdX 14 | ![login](docs/edx-signin.png) 15 | 16 | #### 2. Within EdX course, learner launches Discourse with LTI 17 | ![login](docs/edx-lti-launch.png) 18 | 19 | #### 3. The learner is authenticated with their EdX username 20 | ![login](docs/signedin-forum.png) 21 | 22 | #### 4. The Discourse instance is private 23 | ![login](docs/home.png) 24 | 25 | #### 5. Login button links back to EdX course or allows admin login 26 | ![login](docs/login-dialog.png) 27 | 28 | 29 | ## Course author user experience 30 | #### 1. In Studio's Advanced Settings, enable LTI and add LTI passport (see [EdX docs](http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/lti_component.html)) 31 | ![login](docs/lti-setup.png) 32 | 33 | #### 2. Add a new LTI widget 34 | ![login](docs/lti-component.png) 35 | 36 | #### 3. Set the LTI URL to `/auth/lti/callback` always 37 | ![login](docs/lti-url.png) 38 | 39 | #### 4. Set the LTI Custom Parameters to include `["url=/page-to-link-to"]` 40 | Note that this may be handled differently on edX versus an Open edX instance. See [#22](https://github.com/mit-teaching-systems-lab/discourse-edx-lti/issues/22#issuecomment-350617779 ) for more. 41 | ![login](docs/custom-params.png) 42 | 43 | #### 5. Set the LTI Launch Target to open in a new window 44 | ![login](docs/open-new-window.png) 45 | 46 | 47 | ## Setup 48 | #### Initial setup for new course forums 49 | - This repository assumes you've already done this. See [mit-teaching-systems-lab/discourse-for-moocsters](https://github.com/mit-teaching-systems-lab/discourse-for-moocsters) for setup instructions for how we do this in our labs at MIT. 50 | 51 | #### Install and setup this plugin 52 | - The intent is that the site is private, and learners can only gain access by signing in through EdX and launching the site through LTI. Admin users sign into Discourse directly. 53 | - To do this, the plugin sets some admin site settings related to users and login, which you can see in [config/settings.yml](config/settings.yml). You can edit these in the Discourse Admin UI, but note that the interactions between these settings in different parts of the product are complex, and we don't recommend changing these defaults. 54 | - Install this repository as a Discourse plugin ([instructions](https://meta.discourse.org/t/install-a-plugin/19157)) 55 | - Rebuild container 56 | - Test! Logout from your admin user, and click the Login button. You should see a `Login with EdX` button at the top of the Login dialog box (which won't work yet). 57 | 58 | #### Discourse plugin setup 59 | - Pick an id for the forum site, generate a consumer key and secret 60 | - In Discourse, visit `Admin` -> `Plugins` -> `discourse-edx-lti` 61 | - Set the LTI consumer key and secret, and the EdX course URL that has the LTI button to the forums. 62 | 63 | #### EdX course setup 64 | - In EdX Studio, visit `Settings` > `Advanced settings` 65 | - Add "lti" and "lti_consumer" to `Advanced Module List` 66 | - Add the forum site's id, consumer key and consumer secret to `LTI Passports` 67 | - In Studio, add an LTI Consumer. Set the LTI id, and set the LTI URL to `/auth/lti/callback` on the Discourse forum domain. We typically set the LTI Launch Target to "New Window". 68 | - For the LTI Consumer, make sure to set "Request users' username" and "Request user's email" to `true`. You may need to reach out to someone at EdX to enable this for your course. 69 | 70 | #### Configure your Discourse forums 71 | - Invite any other admin users 72 | - [Configure](https://github.com/discourse/discourse/blob/master/docs/ADMIN-QUICK-START-GUIDE.md) whatever else you like or add some more [plugins](https://meta.discourse.org/c/plugin) or [features](https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#add-more-discourse-features)! 73 | 74 | 75 | ## Local development 76 | You can develop with Vagrant ([see Discourse docs](https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md)). As you develop, clear the ERB cache, copy this repository to the `plugins` folder and restart Rails to see changes. 77 | 78 | Example: 79 | ``` 80 | rm -rf tmp/cache && \ 81 | rm -rf ./plugins/discourse-edx-lti/ && \ 82 | rsync -av --exclude .git \ 83 | ~/github/mit-teaching-systems-lab/discourse-edx-lti \ 84 | ./plugins/ && \ 85 | vagrant ssh -c 'cd /vagrant && bundle exec rails s -b 0.0.0.0' 86 | ``` 87 | 88 | The `plugin-third-party.js.erb` file is what ultimately injects the JavaScript needed to show the new login button in the UI. In development mode, this file will be cached and won't updated if you are rebuilding the plugin on each change. You can touch it manually or just clear the ERB cache on each change (like above). See https://meta.discourse.org/t/tmp-cache-needs-to-be-manually-cleared-when-developing-plugins/17109 or https://github.com/sstephenson/sprockets/issues/563 for other alternatives. 89 | -------------------------------------------------------------------------------- /lti_strategy.rb: -------------------------------------------------------------------------------- 1 | # OmniAuth strategy. By adding it in this namespace, OmniAuth will load it when 2 | # we ask for the :lti provider. 3 | require 'ims/lti' 4 | 5 | # This is from the docs in https://github.com/instructure/ims-lti 6 | require 'oauth/request_proxy/rack_request' 7 | 8 | module OmniAuth 9 | module Strategies 10 | class Lti 11 | include OmniAuth::Strategy 12 | 13 | # These are the params that the LTI Tool Provider receives 14 | # in the LTI handoff. The values here are set in `callback_phase`. 15 | uid { @lti_provider.user_id } 16 | info do 17 | { 18 | edx_username: @lti_provider.lis_person_sourcedid, 19 | email: @lti_provider.lis_person_contact_email_primary, 20 | roles: @lti_provider.roles, 21 | resource_link_id: @lti_provider.resource_link_id, 22 | context_id: @lti_provider.context_id 23 | } 24 | end 25 | extra do 26 | { :raw_info => @lti_provider.to_params } 27 | end 28 | 29 | def callback_phase 30 | # Rescue more generic OAuth errors and scenarios 31 | begin 32 | log :info, 'callback_phase: start' 33 | @lti_provider = create_valid_lti_provider!(request) 34 | 35 | log :info, "lti_provider.custom_params: #{@lti_provider.custom_params.inspect}" 36 | set_origin_url!(@lti_provider.custom_params) 37 | super 38 | rescue ::ActionController::BadRequest => err 39 | log :info, "lti_provider.bad_request, params: #{request.params.inspect}, err: #{err.inspect}" 40 | return [400, {}, ['400 Bad Request']] 41 | rescue ::Timeout::Error => err 42 | log :info, "lti_provider.Timeout::Error, params: #{request.params.inspect}, err: #{err.inspect}" 43 | fail!(:timeout) 44 | rescue ::Net::HTTPFatalError, ::OpenSSL::SSL::SSLError => err 45 | log :info, "lti_provider.Net::HTTPFatalError, params: #{request.params.inspect}, err: #{err.inspect}" 46 | fail!(:service_unavailable) 47 | rescue ::OAuth::Unauthorized => err 48 | log :info, "lti_provider.OAuth::Unauthorized, params: #{request.params.inspect}, err: #{err.inspect}" 49 | fail!(:invalid_credentials) 50 | rescue ::OmniAuth::NoSessionError => err 51 | log :info, "lti_provider.OmniAuth::NoSessionError, params: #{request.params.inspect}, err: #{err.inspect}" 52 | fail!(:session_expired) 53 | rescue ::ActiveRecord::RecordInvalid => err 54 | log :info, "lti_provider.ActiveRecord::RecordInvalid, params: #{request.params.inspect}, err: #{err.inspect}" 55 | fail!(:record_invalid) 56 | end 57 | end 58 | 59 | protected 60 | def log(method_symbol, text) 61 | Rails.logger.send(method_symbol, "LTIStrategy: #{text}") 62 | end 63 | 64 | # Creates and LTI provider and validates the request, returning 65 | # an IMS LTI ToolProvider. Raises ActionController::BadRequest if it fails. 66 | def create_valid_lti_provider!(request) 67 | if request.request_method != 'POST' 68 | log :info, "Request method unsupported: #{request.request_method}" 69 | raise ActionController::BadRequest.new('Unsupported method') 70 | end 71 | 72 | # Check that consumer key is what we expect 73 | credentials = read_credentials() 74 | request_consumer_key = request.params['oauth_consumer_key'] 75 | log :info, "Checking LTI params for consumer_key #{credentials[:consumer_key]}: #{request.params}" 76 | if request_consumer_key != credentials[:consumer_key] 77 | log :info, 'Invalid consumer key' 78 | raise ActionController::BadRequest.new('Invalid request') 79 | end 80 | 81 | # Create provider and validate request 82 | lti_provider = IMS::LTI::ToolProvider.new(credentials[:consumer_key], credentials[:consumer_secret], request.params) 83 | if not lti_provider.valid_request?(request) 84 | log :info, 'lti_provider.valid_request? failed' 85 | raise ActionController::BadRequest.new('Invalid LTI request') 86 | end 87 | 88 | lti_provider 89 | end 90 | 91 | # This uses Discourse's SiteSetting for configuration, which can be changed 92 | # through the admin UI. Using OmniAuth's nice declarative syntax for credential options 93 | # means those values need to be passed in at app startup time, and changes in the admin 94 | # UI don't have an effect until restarting the server. 95 | def read_credentials 96 | { 97 | consumer_key: SiteSetting.lti_consumer_key, 98 | consumer_secret: SiteSetting.lti_consumer_secret 99 | } 100 | end 101 | 102 | # Respect the "url" custom parameter in EdX and make sure we redirect to it 103 | # after authentication. This allows learners to click an LTI link and jump 104 | # directly to a particular page. 105 | # 106 | # Typical OmniAuth strategies expect all URLs to redirect to a login 107 | # page, and then thread the origin URL through the OAuth process as the 108 | # `origin` query param. LTI expects to be able to post to a single URL, and 109 | # pass params about where to navigate to afterward. If we passed the `origin` 110 | # query param, this would work with OmniAuth and Discourse to a certain point, 111 | # but the EdX Studio UI requires query string params to be properly escaped, 112 | # which is a barrier for course authors. So we work around by having authors 113 | # set `["url=https://foo.com/whatever"]` in EdX studio, and read that here. 114 | # 115 | # Unfortunately, Discourse checks `omniauth.origin` but overrides whatever it finds 116 | # there if a :destination_url cookie is set (see omniauth_callbacks#complete), and 117 | # in the LTI path it will be set to the root URL. So here we set that cookie directly, 118 | # which Discourse reads in the controller and redirects to after finishing the 119 | # authentication process. 120 | # 121 | # Since the request is LTI-signed, this is secure, but Discourse will 122 | # parse it and discard the domain to be safe. 123 | def set_origin_url!(lti_custom_params) 124 | origin_url = lti_custom_params['url'] 125 | return unless origin_url 126 | 127 | log :info, "set_origin_url: #{origin_url}" 128 | @env['action_dispatch.cookies'][:destination_url] = origin_url 129 | end 130 | end 131 | end 132 | end --------------------------------------------------------------------------------