Quarkus: `JsonWebToken` always null inside Uni/ Multi callback

Created on 7 May 2020  路  19Comments  路  Source: quarkusio/quarkus

Describe the bug
I'm building API w/ quarkus-vertx-web & quarkus-smallrye-jwt. When I try call get claims values fromJsonWebToken inside Uni/ Multi callback they're always null like this:

@Route(path = "/tickets/:id", methods = HttpMethod.GET)
  void getTicket(RoutingContext ctx) {
    UUID id = RouteUtil.getUUIDParams(ctx, "id");
    log.info(">>>>>>>> " + jwt.getName()); // <== this return correct value
    ticketSvc.getOneById(id)
        .subscribe()
        .with(ThrowingConsumer.unchecked(ticket -> {
              if (ticket == null) {
                ctx.fail(HttpStatus.SC_NOT_FOUND);
              } else if (!Objects.equals(ticket.getUserId(), jwt.getName())) {
                ctx.fail(HttpStatus.SC_FORBIDDEN);
              } else {
                log.info("<<<<<<<<< " + jwt.getName());  // <== this return null
                ctx.response()
                    .setStatusCode(HttpStatus.SC_OK)
                    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
                    .end(mapper.writeValueAsString(ticket));
              }
            }),
            ctx::fail);
  }

Expected behavior
Should return correct claims value

Configuration

# Add your application.properties here, if applicable.
mp.jwt.verify.publickey.location=https://${AUTH0_DOMAIN}/.well-known/jwks.json
mp.jwt.verify.issuer=https://${AUTH0_DOMAIN}/
smallrye.jwt.path.groups=permissions
smallrye.jwt.groups-separator=','
smallrye.jwt.always-check-authorization=true

arereactive aresmallrye arevertx kinbug

Most helpful comment

Vert.x 3.9.1 was released, so as soon as we upgrade in Quarkus, we can use it to fix this bug.

All 19 comments

This looks like a context propagation issue. Perhaps jwt is a request-scoped bean?

Yes, it is request scoped

In that case you need context propagation, but with Uni it should be automatic, no, @cescoffier ?

@KienDangTran You've mentioned in Zulip that it works in one part of your code while does not work above. Can you please post the complete code showing where it works and where it does not

I already created a similar issue #8538

It has a reproducer

@sberyozkin As I mentioned, It works when it's called outside Uni/Multi callback handler and doesnt work when it's inside. Here are sample code:

@Slf4j
@RequestScoped
@RouteBase(path = "/base")
public class TicketRoutes {
  private final ObjectMapper mapper;
  private final TicketService ticketSvc;
  private final JsonWebToken jwt;

  public TicketRoutes(TicketService ticketSvc, JsonWebToken jwt) {
    this.ticketSvc = ticketSvc;
    this.jwt = jwt;
    this.mapper = DatabindCodec.mapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .setDefaultPropertyInclusion(
            JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.ALWAYS))
  }

@Route(path = "/tickets", methods = HttpMethod.POST, consumes = "application/json")
  void postTicket(RoutingContext ctx) throws IOException {
    Ticket ticket = mapper.readValue(ctx.getBody().getBytes(), Ticket.class);

    log.info( // <= this returns correct claim values
        "Issuer: {}\nName: {}\nGroups: {}\nAudience: {}",
        jwt.getIssuer(),
        jwt.getName(),
        jwt.getGroups(),
        jwt.getAudience());

    ticketSvc
          .createTicket(ticket)
          .subscribe()
          .with(
              createdTicket ->
                    ctx.response()
                        .setStatusCode(HttpStatus.SC_CREATED)
                        .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
                        .end(mapper.writeValueAsString(createdTicket)),
              ctx::fail);
  }
}

@Slf4j
@RequestScoped
public class TicketService {
  private final TicketRepository ticketRepo;
  private final JsonWebToken jwt;

  public TicketService(TicketRepository ticketRepo, JsonWebToken jwt) {
    this.issueRepo = issueRepo;
    this.ticketRepo = ticketRepo;
    this.jwt = jwt;
  }

  public Uni<Ticket> createTicket(Ticket ticket) {
    log.info( // <= this returns NULL values
        "Issuer: {}\nName: {}\nGroups: {}\nAudience: {}",
        jwt.getIssuer(),
        jwt.getName(),
        jwt.getGroups(),
        jwt.getAudience());

    return ticketRepo.create(ticket.withUserId(jwt.getName())).map(ticket::withId);
  }
}

@KienDangTran You've mentioned in Zulip that it works in one part of your code while does not work above. Can you please post the complete code showing where it works and where it does not

OK, I've investigated and this works if you import the extension quarkus-smallrye-context-propagation AND you use RESTEasy.

When using reactive routes, it appears context propagation doesn't work, and I guess it's perhaps because something is terminating the request context prematurely. Could that be the case @mkouba ?

To be clear, using reactive routes, context propagation works for other contexts than the ArC request scope.

I guess it's perhaps because something is terminating the request context prematurely. Could that be the case?

AFAIK the request context is terminated when the route method completes. So yes, it's very likely the case.

@FroMage @cescoffier so I wonder if we should terminate the request context unless a route method returns Uni or Multi? What else is needed to support the context propagation properly?

Yes, it should be terminated when the Uni/Multi "terminates" (on().termination())

Yup, this is due to RouteHandler:

            try {
                requestContext.activate();
                if (user != null) {
                    Arc.container().beanManager().fireEvent(user.getSecurityIdentity());
                }
                invoke(context);
            } finally {
                requestContext.terminate();
            }

My advice is that you should register a handler for when the request is over:

    @Override
    default void handle(RoutingContext context) {
        QuarkusHttpUser user = (QuarkusHttpUser) context.user();
        ManagedContext requestContext = Arc.container().requestContext();
        //todo: how should we handle non-proactive authentication here?
        if (requestContext.isActive()) {
            if (user != null) {
                Arc.container().beanManager().fireEvent(user.getSecurityIdentity());
            }
            invoke(context);
        } else {
            requestContext.activate();
            context.response().endHandler(v -> requestContext.terminate());
            if (user != null) {
                Arc.container().beanManager().fireEvent(user.getSecurityIdentity());
            }
            invoke(context);
        }
    }

Actually it's a little bit more complicated than this, because calling endHandler might override the previous handler silently. Hence why https://github.com/vert-x3/vertx-web/pull/1593 will support multiple handlers, and why @stuartwdouglas wrote https://github.com/quarkusio/quarkus/blob/4239c357540c0d19d96095ff0ebc98affe950ace/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/ShutdownRequestWrapper.java#L58

We could wrap the request (or response) to do like Stuart, but I'm not sure where in this case.

@stuartwdouglas you have an idea?

Apparently this is coming soon in vert.x 3.9.1, so maybe just wait?

Note that it still needs to be deactivated after the request, just not terminated.

Yup. OK let's wait.

Vert.x 3.9.1 was released, so as soon as we upgrade in Quarkus, we can use it to fix this bug.

Vert.x 3.9.1 is now in master so we can fix this bug.

@FroMage I'm on it...

Great, thanks a lot.

Was this page helpful?
0 / 5 - 0 ratings