How to Validate LTI Requests with a Custom Warden Strategy
Authorization with LTI
In a Rails application, have you ever found yourself wanting various ways to authenticate incoming requests based on some kind of conditional?
Well, neither did I until the introduction of LTI (Learning Tools Interoperability). We’ll dive into what that is in a second, but first, let’s think about some use cases for supporting multiple authorization strategies. Maybe you want to handle the authorization of some API endpoints differently from the rest, or you want the authorization process to change depending on the specific params provided, or you might even want to change the authorization flow depending on the environment the app is in.
I found myself in the latter scenario recently when we were given a new requirement that all incoming requests in a specific environment only had to follow the LTI standard. All other environments would continue using our existing credentials + password auth flow, so we couldn’t replace it altogether. Instead, we needed to support two different auth flows side-by-side… *Warden Strategies enter stage right*
A brief overview of Warden Strategies
For us, the discovery of Warden Strategy was invaluable. So what is Warden? Warden is a gem that can be used in Rack-based Ruby applications to introduce authentication systems in the app’s middleware. Warden provides session handling and uses a rack object to set auth information (including the user object).
A Warden Strategy houses the logic for request authentication. Multiple “cascading strategies” can be used in a single application. Warden will run through all existing strategies until one succeeds, fails, or if none are found applicable for the particular request. A warden strategy often consists of at least two methods:
#valid?
(Optional method) — provides a guard against inapplicable strategies
Evaluates to true or false; if true, the strategy will run. If false, strategy will not run. Without this method, the strategy will run by default#authenticate!
(Required method) — executes the authentication logic
Can either halt the strategy stack or pass onto the next strategy (passing occurs by default if the halt! action is not called)
All strategies have request-related methods and action methods inherited from Warden::Strategies::Base
that can be used within the strategy. These methods can trigger the halting of the strategy stack or ‘login’ a user if authentication is successful.
Example strategy below:
Warden::Strategies.add(:signature) do
def valid?
params['signature']
end
def authenticate!
user = user_from_decoded_signature(params['signature'])
if user.present?
halt!
else
pass
end
end
end
Our custom LTI Warden Strategy
Now that we have a little background on Warden Strategies, let’s dive into the custom strategy we created to validate the LTI requests. To start, LTI is a protocol that allows “learning tools” (applications) to securely communicate with each other. An LTI request is just a POST request with pre-defined params, including an OAuth signature, shared key, and other OAuth/request information. For our specific case, only valid LTI requests should be filtered through. Therefore, the authentication strategy for these requests should be the first and only strategy run, so we add the strategy, and then ensure it is the first in the stack:
warden_strategies.add(:lti_auth, LtiAuth::WardenStrategy)
warden_config.default_strategies(scope: :user).unshift(:lti_auth)
NOTE: we opted for housing the lti_auth
strategy logic in a separate class — LtiAuth::WardenStrategy
— as opposed to a do block as seen above)
Now that we’ve added the strategy, we want to ensure that it is only ever run given the appropriate environment:
module LtiAuth
class WardenStrategy < Warden::Strategies::Base
def valid?
LtiAuth.on?
end... (code continued)
We do this using the #valid?
method. In this particular case, if LtiAuth.on?
is true, the strategy will run; otherwise, it will not.
If we are in an environment in which this strategy should be used, the next method called is #authenticate!
. This is where the authentication logic gets executed. As mentioned before, LTI request parameters include an OAuth signature. Because Warden provides access to request methods, we are able to pass the request information to the LtiAuth::RackOauthVerifier
, which validates the authenticity of the signature (an encoded combination of the request method, url, params, and a shared key and secret). We also want to ensure that required user credentials are provided. If both the LTI signature is valid and the correct user params exist, we go ahead and provision the user.
... def authenticate!
return handle_failure(forbidden) unless valid_credentials?
provision_user
endprivate def provision_user
user = Provisioning::LtiUser.find_or_create_by(email: email, user_id: user_id)
return handle_failure(not_found) if user.blank?
success!(user)
end def handle_failure(response)
custom!(response)
end def valid_credentials?
valid_lti? && valid_params?
end def valid_lti?
LtiAuth::RackOauthVerifier.validate_signature(request.url, request.request_parameters)
end def valid_params?
user_id.present? && email.present?
end... (code continued)
Okay cool, that’s great and all, but how do we stop any following strategies from executing and how do we get logged in? Great question. As mentioned above, Warden provides action strategy methods. The #halt!
method does exactly that: it halts the cascading of strategies. As used above, both #success!
and #custom!
trigger a #halt!
. If successfully validated, the #success!
method receives the authenticated user object and sets the session and user information in the warden rack object. If authentication fails, the #custom!
method receives a custom rack array and sends this array as the response to the request without moving forward into the application. For more information on these actions, including #fail!
, #redirect!
, and #pass
, checkout again the Warden Wiki.
And that’s it! We’ve authenticated our request and the user is logged in. So we’re done, right? Well, not quite.
With a session set, Warden will skip all strategies upon subsequent requests. However, what if I want to verify if the session user is the appropriate user and that the OAuth signature provided is still valid? *Enter #after_set_user
stage left*
warden_manager.after_set_user do |user, auth, opts|
LtiAuth::WardenAfterSetUser.new(user, auth, opts).execute
end
Warden provides us with a warden_manager
object. This object provides an #after_set_user
method that gets called every time after a user is set upon request or when a user is set based on preexisting session data upon subsequent requests. We created a LtiAuth::WardenAfterSetUSer
class to help us validate the user and the provided signature.
module LtiAuth
class WardenAfterSetUser
attr_reader :user, :auth, :opts, :request def initialize(user,auth,opts)
@user = user
@auth = auth
@opts = opts
@request = Rack::Request.new(auth.session_serializer.env)
end def execute
return false unless LtiAuth.on?
auth.logout if logged_in_auth_failure?
endprivate def logged_in_auth_failure?
invalid_user? || invalid_signature?
end def invalid_user?
user.learn_uuid != request.params["user_id"]
end def invalid_signature?
!LtiAuth::RackOauthVerifier.validate_signature(request.url, request.params)
end end
end
If both are correct, we go ahead and proceed as usual. If either the user is invalid or the signature is invalid, logout the user. Logging out a user will trigger the cascading strategy stack to be run again. So our lovely authentication strategy has another stab at making things right.
Work better with diagrams? Same. Here are the same authentication flows discussed above in a nicely illustrated visual format, to help round things out.
Resources:
Thanks for reading! Want to work on a mission-driven team that loves LTI and flexible authorization strategies? We’re hiring!
To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.
Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.