rubyonjets / jets

Ruby on Jets
http://rubyonjets.com
MIT License
2.6k stars 181 forks source link

Jets doesn't seem to create lambdas and/or log groups for routes created by gems/middleware #566

Closed sam0x17 closed 3 years ago

sam0x17 commented 3 years ago

Checklist

My Environment

Software Version
Operating System Ubuntu 20.04
Jets 3.0.9
Ruby 2.7.2

Expected Behaviour

Any routing/endpoints defined by middleware (like omniauth) should run in one of the deployed lambdas, and should have an associated log group.

Current Behavior

As far as we can tell, routes/endpoints that are defined by middleware (like omniauth) do not get mapped to a particular lambda or in the very least aren't traceable in logs. I know this because we are getting a 500 on an omniauth endpoint when we hit it, but we can't find any logs corresponding with this 500 (or corresponding with any middleware-created endpoints).

Step-by-step reproduction instructions

Set up a blank jets project that includes a gem that defines some sort of route. In our case we are using omniauth via Auth0, however you could probably reproduce this with Sinatra for example.

Code Sample

We have a minimal setup reproducing this bug located here https://github.com/rachaelghorbani/auth0-test

Solution Suggestion

My guess is Jets is only aware of routes that are defined in routes.rb. Perhaps allow defining routes in routes.rb that should be handled by middleware, and then wire these into API Gateway properly. We don't mind having to manually annotate these routes as long as the middleware can still handle them.

tongueroo commented 3 years ago

Thanks for putting together the test repo. Deployed it and the controller lambda functions for the auth0_controller.rb are being created, at least when I tested. 🧐

Screen Shot 2021-06-29 at 7 56 52 PM

Haven't really dig into omniauth in quite a while. Believe the workflow is that:

  1. You try to log into a protected page
  2. Omniauth will redirect you to a 3rd party page that handles auth/login
  3. You login and that 3rd party login provider is configured so that it knows to call your auth0_controller.rb as a callback/hook. The 3rd party provider passes auth information, which the omniauth provider stores in the request env via the middleware. This is made available with something like request.env['omniauth.auth'] So you can store this session info and consider the user logged in.

The logs from the middleware will log in the same process as the calling lambda function. In this case, the lambda functions are the ones in the screenshots above. IE: Auth0Test-dev-auth0_controller-callback

It looks like the omniauth default logger is STDOUT, which should work. https://github.com/omniauth/omniauth/blob/a62d36b3f847e0e55b077790112e96950c35085a/lib/omniauth.rb#L28

The jets default logger writes to stderr which also shows up in CloudWatch: https://github.com/boltops-tools/jets/blob/b83e7606e7009037cef69be27ed784b31f3dbf32/lib/jets/application/defaults.rb#L53

You might want to configure the Omniauth logger to the same logger as Jets:

OmniAuth.config.logger = Jets.logger

Though omniauth logging to stdout should still show up just fine in the specific lambda function CloudWatch logs.

Haven't been able to dug into omniauth much yet, so there might be other caveats. Unsure if there's much more here. Hopefully, the explanation helps though.

sam0x17 commented 3 years ago

Thanks a bunch @tongueroo. I did some more digging based on what you said and I found the error. It looks like API Gateway doesn't like something about how omniauth is formatting its lambda response:

Thu Jul 01 07:05:04 UTC 2021 : Endpoint response body before transformations: {"statusCode":302,"body":["302 Moved"],"isBase64Encoded":false,"headers":{"Location":"/auth/failure?message=missing_client_id&strategy=auth0","Set-Cookie":"rack.session=truncated; path=/; HttpOnly","X-Runtime":"0.001454","x-jets-call-count":"20","x-jets-prewarm-count":"1"}}
Thu Jul 01 07:05:04 UTC 2021 : Execution failed due to configuration error: Malformed Lambda proxy response
Thu Jul 01 07:05:04 UTC 2021 : Method completed with status: 502

Any idea how to fix this?

My guess is it doesn't like that body is ["302 moved"] instead of a string. I have no idea how to induce jets to stringify this / where that code is, however.

When this happens, the response from API gateway is just the generic {"message": "Internal server error"} which I thought was API gateway complaining that the path isn't routed. My mistake!!

sam0x17 commented 3 years ago

I'm suspicious that it might be something like this: https://stackoverflow.com/a/50263560/4245513

tongueroo commented 3 years ago

Dug into this a bit. Havent messed with omniauth-like workflows in a while so was unable to set it up to the point where omniauth reproduces the 302 Moved redirect.

Nevertheless mimicked it with a piece of middleware:

app/models/my_middleware.rb

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    Jets.logger.info("middleware test")
    status, headers, body = @app.call(env)
    body = ["302 Moved"] # mimic the issue
    [status, headers, body]
  end
end

And configure it in

config/application.rb

require "#{Jets.root}/app/models/my_middleware"
Jets.application.configure do
  # ...
   config.middleware.use MyMiddleware
end

This reproduces the API Gateway error {"message": "Internal server error"}. So think you're right, the ["302 moved"] causes API Gateway to error. API Gateway is pickier about the response body. The standard is that with 302 there should be no body or guessing an empty string.

Think either have to:

  1. Dig into the omniauth and edit it so that it does not add the ["302 moved"]. Unsure where that is and that patch may not be accepted by omniauth 😞
  2. Possibly add another piece of middleware after omniauth that replaces ["302 moved"] with ""

Think would give #2 a try. Hoping the middleware above is a starter example. Hope that helps. 🀞

FWIW, auth needs some love for Jets. Some more thoughts here: https://community.boltops.com/t/current-state-of-building-authentication-and-authorization-in-jets/691

sam0x17 commented 3 years ago

Thanks @tongueroo this is great and in line with my thinking as well. I will try monkey-patching and report back here, but thanks so much for taking the time to reproduce the issue!!

sam0x17 commented 3 years ago

This seems to work:

class OmniauthFix
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body = body.join(', ') if [301, 302].include?(status) && body.is_a?(Array)
    [status, headers, body]
  end
end

Thanks again!