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).

More on the what, why, and how in terms of use and setup can be found on the Warden Wiki.

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
end
private
   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_userstage 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?
end
private
    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.

A user makes a launch request with a valid signature and user params. The request hits the Warden strategy and credentials are validated, setting the user. After set user is triggered and all checks on user information pass, allowing the request to hit the controller.
A user makes a launch request with an invalid signature or missing/invalid user params. The request hits the Warden strategy and validation fails. An error response is sent back to the client without having gotten to the application.
A user makes a subsequent launch request. The request skips the Warden strategy and a user is set based off the pre-existing session data. After set user is triggered, when the OAuth signature is validated and a check is made that the requesting user and session user are the same, the request successfully hits the application
A user makes a subsequent launch request. The request skips the Warden strategy and a user is set based off the pre-existing session data. After set user is triggered. When the OAuth signature is invalid, the user will be logged out of the session. After logging out, the Warden Strategy gets triggered and will fail due to the invalid signature, returning a 403 response.
A user makes a subsequent launch request. The request skips the Warden strategy and a user is set based off the pre-existing session data. After set user is triggered. When the OAuth signature is valid, but the session user and requesting user are different, the session user is logged out. After logging out, the Warden Strategy gets triggered, the signature is valid and the requesting user is set as the new session user — successfully passing through the Warden After Set User and to the application.

Unfortunately no, I do not understand why googling Warden results in a plethora of Fortnight related gifs


Thanks for reading! Want to work on a mission-driven team that loves LTI and flexible authorization strategies? We’re hiring!


Footer top

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.

Footer bottom