Does anybody have any patterns for trying two or more similar transactions? I want to return Success from first transaction that succeeds, but should the all fail, I want to know which step failed.
Our use-case is that we perform authentication via JWT bearer tokens, primarily through Auth0, but we’ve recently had to add support for our own. Both use the same style JWT, but need to be verified using different keys.
We need to return a different response based on how the JWT failed:
- Missing or bogus JWT -> 401 Not Authorized
- Valid JWT, but no or inactive user -> 403 Forbidden
- Valid user -> Success
That is, a 401 is “I don’t know who you are”, and 403 is “I know who you are, but you’re not allowed”.
Previously when we had only a single Auth0 Authenticate transaction, we had distinct steps for “decode+verify JWT” and “find the user”. Then, the controller could use the monad block matcher to trigger different status code responses based on the step that failed.
Now that we’ve added another flavor of JWT with different configs, I created another transaction that works just like the original, and joined them together with #or
. The nature of transactions, though, means if they both fail, the failure that gets returned from the outer transaction has discarded the step that failed in the inner.
Here’s the code that shows the intent, but doesn’t work because the on.failure(:verify_jwt_token)
never matches.
class Authentication::DecodeToken
step :decode_bearer_token
def decode_bearer_token(payload)
decode_auth0 = Authentication::DecodeAuth0Jwt.new(auth0: @auth0)
decode_api_token = Authentication::DecodeApiTokenJwt.new(config: bar)
decode_auth0.call(payload).or { decode_api_token.call(payload) }
end
class Authentication::DecodeAuth0Jwt
def initialize(auth0_config:, **)
@auth0_config = auth0_config
super
end
step :verify_jwt_token
step :validate_payload
map :user_for_token
end
class ApiController
def current_user
@current_user ||= authenticate
end
def authenticate
authenticate_with_http_token do |token, options|
Authenticate
.new(auth0_config: Rails.config.auth0)
.call(token: token, **options) do |on|
on.failure(:verify_jwt_token) { raise Forbidden }
on.failure { raise NotAuthenticated }
on.success { |user| user }
end
end
end
end
end
The not-great solution I’ve come up with is to handle the Failure inside the or
block, return a different failure, and then add a case statement inside the failure branch in the controller.
# decode_bearer_token
decode_auth0.call(payload).or do
decode_api_token.call(payload) do |on|
on.failure(:verify_jwt_token) { Failure(:invalid_token) }
on.failure { |err| Failure(err) }
on.success { |value| value }
end
end
# controller
.call(token: token, **options) do |on|
on.success { |user| user }
on.failure do |error|
case error
when :invalid_token then raise Forbidden
else
raise NotAuthenticated
end
end
end
I’m not thrilled with this approach, I don’t really like having to handle the inner transactions Failure just to wrap it in a different failure, and then using a case
to decide what kind of failure it was. Having the StepFailure exposed to the do
block, instead of the Failure it wraps (as I mentioned in Ideas awhile back) would probably help with this a lot.