quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.8k stars 2.68k forks source link

Custom IdentityProvider not getting called in Quarkus amazon lambda http api when deployed to AWS #24609

Open faskan opened 2 years ago

faskan commented 2 years ago

Describe the bug

My custom identity provider is not getting called when a quarkus lambda http api is deployed in AWS. My test case works fine. And in SAM local it works only when setting QUARKUS_AWS_LAMBDA_FORCE_USER_NAME environment variable. In AWS it doesn't work at all. When I curl the API gateway endpoint, I simply get a NullPointerException on my resource class just because the security context is null. I added loggers to my custom identity provider and found that the provider is not getting called.

I am using the quarkus generated sam.*.yaml file to deploy my lambda to AWS. I've tried both jvm and native sam files, but both produce the same result.

I am expecting quarkus to automatically discover any custom identity provider and invoke it in Lambda authentication mechanism. I did some investigation and I am a bit puzzled about the below condition in LambdaHttpAuthenticationMechanism.isAuthenticatable method.

event.getRequestContext() != null && event.getRequestContext().getAuthorizer() != null

It seems like my custom identity provider is not getting called because of this condition. Is quarkus expecting a custom lambda authorizer on top of the custom LambdaIdentityProvider?

Here is my complete source code. The below property is enabled in application.properties quarkus.lambda-http.enable-security=true

@ApplicationScoped
public class MBizSecurityProvider implements LambdaIdentityProvider {

    @Override
    public SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) {
        if (event.getHeaders() == null || !event.getHeaders().containsKey("x-user")) {
            throw new RuntimeException("No auth header");
        }
        if(!event.getHeaders().get("x-user").equalsIgnoreCase("test")) {
            throw new RuntimeException("Invalid user");
        }
        Principal principal = new QuarkusPrincipal(event.getHeaders().get("x-user"));
        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
        builder.setPrincipal(principal);
        return builder.build();
    }
}
@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext securityContext) {
        return "hello " + securityContext.getUserPrincipal().getName();
    }
}
@QuarkusTest
public class GreetingTest {
    @Test
    public void test() {
        APIGatewayV2HTTPEvent request = request("/hello");
        request.setHeaders(ImmutableMap.of("x-user", "test"));
        request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer());

        given()
                .contentType("application/json")
                .accept("application/json")
                .body(request)
                .when()
                .post(AmazonLambdaApi.API_BASE_PATH_TEST)
                .then()
                .statusCode(200)
                .body("body", equalTo("hello test"));
    }

    private APIGatewayV2HTTPEvent request(String path) {
        APIGatewayV2HTTPEvent request = new APIGatewayV2HTTPEvent();
        request.setRawPath(path);
        request.setRequestContext(new APIGatewayV2HTTPEvent.RequestContext());
        request.getRequestContext().setHttp(new APIGatewayV2HTTPEvent.RequestContext.Http());
        request.getRequestContext().getHttp().setMethod("GET");
        return request;
    }
}

Expected behavior

Custom Identity provider should be called

Actual behavior

Custom identify provider is not getting called.

How to Reproduce?

  1. Clone this repo https://github.com/faskan/mbiz-quarkus-lambda-http-apis
  2. quarkus build
  3. sam build -t target/sam.jvm.yaml
  4. sam deploy --guided
  5. Do a GET api call on /hello with header x-user=test

Output of uname -a or ver

No response

Output of java -version

No response

GraalVM version (if different from Java)

No response

Quarkus version or git rev

2.7.5.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.2

Additional information

No response

quarkus-bot[bot] commented 2 years ago

/cc @matejvasek, @patriot1burke

faskan commented 2 years ago

Additional information: Note that it works fine with quarkus amazon rest api (quarkus-amazon-lambda-rest). I could see that the rest api implementation has an extra conditionevent.getRequestContext().getIdentity() != null that makes it work. event.getRequestContext().getAuthorizer() comes null for rest api as well.

private boolean isAuthenticatable(AwsProxyRequest event) {
        Map<String, String> systemEnvironment = System.getenv();
        boolean isSamLocal = Boolean.parseBoolean((String)systemEnvironment.get("AWS_SAM_LOCAL"));
        String forcedUserName = (String)systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME");
        return isSamLocal && forcedUserName != null || event.getRequestContext() != null && (event.getRequestContext().getAuthorizer() != null || event.getRequestContext().getIdentity() != null);
    }

I am not sure if there is something equivalent to identity available from AWS api gateway for http api

vladaman commented 2 years ago

I am having similar issue. Using AWS API Gateway (HTTP) with JWT + Cognito. We get an exception when we inject:

public class UsersResponseImpl implements UsersApi {
  @Inject
  javax.ws.rs.core.SecurityContext securityContext;

Exception trace:

[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:2.11.3.Final:build (default) on project UsersApi: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR]     [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: javax.enterprise.inject.spi.DeploymentException: javax.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type javax.ws.rs.core.SecurityContext and qualifiers [@Default]
[ERROR]     - java member: com.myapp.UsersResponseImpl#securityContext
[ERROR]     - declared on CLASS bean [types=[com.myapp.openapi.app.api.UsersApi, java.lang.Object, com.myapp.UsersResponseImpl], qualifiers=[@Default, @Any], target=com.myapp.UsersResponseImpl]
[ERROR]     at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1209)
[ERROR]     at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:275)
[ERROR]     at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:134)
[ERROR]     at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:494)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[ERROR]     at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[ERROR]     at java.base/java.lang.reflect.Method.invoke(Method.java:566)
[ERROR]     at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:977)
[ERROR]     at io.quarkus.builder.BuildContext.run(BuildContext.java:281)
[ERROR]     at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
[ERROR]     at java.base/java.lang.Thread.run(Thread.java:829)
[ERROR]     at org.jboss.threads.JBossThread.run(JBossThread.java:501)
[ERROR] Caused by: javax.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type javax.ws.rs.core.SecurityContext and qualifiers [@Default]
[ERROR]     - java member: com.myapp.UsersResponseImpl#securityContext
[ERROR]     - declared on CLASS bean [types=[com.myapp.openapi.app.api.UsersApi, java.lang.Object, com.myapp.UsersResponseImpl], qualifiers=[@Default, @Any], target=com.myapp.UsersResponseImpl]
[ERROR]     at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:411)
[ERROR]     at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:532)
[ERROR]     at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:263)

I also tried to inject java.security.Principal but that does not seem to have any values.

michalvavrik commented 1 year ago

@faskan Thanks for analysis, I'll have a look.

michalvavrik commented 1 year ago

@vladaman I don't think your case is related, however you provided very little information. I'd suggest that you open an issue and ping me there. Anyway, using @Context on resource method should work, e.g.

public String getUsername(@Context SecurityContext ctx) {
        return ctx.getUserPrincipal().getName();
    }
vladaman commented 1 year ago

Thanks @michalvavrik

We've completely moved away from approach above. Rather we refactored our code with AWS HTTP API Gateway integration which does the JWT validation. We just parse APIGatewayV2HTTPEvent event and extract claims.

This works for us and gets job done: https://gist.github.com/vladaman/a7469213521f47575571c30c9b9b5aab

michalvavrik commented 1 year ago

Hey @faskan ,

I think the behavior exactly matches documentation, I'll try to explain why:

Docs here says that with quarkus.lambda-http.enable-security=true you enable security feature to securely invoke HTTP endpoints via API Gateway access control mechanism and table HTTP quarkus-amazon-lambda-http exactly shows where principal is taken from: requestContext.authorizer.jwt.claims.cognito:username or
requestContext.authorizer.iam.userId or requestContext.authorizer.lambda.principalId. If you test it locally, you can force user (as you mentioned), but otherwise only source of principal name are fields above. The feature is all about leveraging API Gateway support for controlling and managing access to your HTTP API. Access control happens before Quarkus lambda is even reached.

Your unit test is passing as you added authorizer, but you already know that.

You expect custom Identity provider to be called, but even custom identity provider needs identity source (in this case - authorizer field). If you want your own custom identity source that doesn't use API Gateway authorizers above, you simply need to use different authentication mechanism, or provide your own. Additionally docs even mention that custom identity provider https://quarkus.io/guides/amazon-lambda-http#custom-security-integration can be used to map security metadata (or request attributes) to security identity as AWS security only maps the principal name to Quarkus security APIs and does nothing to map claims or roles or permissions - hence the purpose of custom identity provider.

I followed Steps to reproduce and added Lambda authorizer that simply checked x-user header and got call passing without NPE.

I am going to close the issue as in my eyes docs is pretty clear and you misunderstood what identity provider does. In case you don't agree or feels we should improve docs, or ... anything - just re-open the issue, no problem! Thanks

brucej72 commented 2 months ago

I think this ticket should be repoened. I can't get a CUSTOM lambda security integration to work at all. According to the docs here I should:

  1. enable the security framework by adding quarkus.lambda-http.enable-security=true to my application.properties
  2. create a CDI bean that implements LambdaIdentityProvider and override the method public SecurityIdentity authenticate(APIGatewayV2HTTPEvent event)

I've done both of these things, and the source for the customer provider is below:

@ApplicationScoped
public class MyCustomProvider implements LambdaIdentityProvider {

    @Inject
    Logger LOG;

    @Startup
    public void onStartup() {

        LOG.info("Starting custom provider");

    }

    @Override
    public SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) {

        LOG.info("authenticate called");

        if (event.getHeaders() == null || !event.getHeaders().containsKey("x-user"))
            return null;
        Principal principal = new QuarkusPrincipal(event.getHeaders().get("x-user"));
        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
        builder.setPrincipal(principal);
        return builder.build();
    }
}

When I start this up in dev mode (using quarkus dev) I see the customer provider being created: 2024-09-11 10:47:04,345 INFO [com.pay.eve.val.MyCustomProvider] (Quarkus Main Thread) Starting custom provider but when I hit the main http function lambda, nothing happens, there is no log message to say "authenticate called". The docs linked about do not mention whether the http function lambda needs to be annotated in some way - my http handler function method signature looks like:

@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response consume(String eventData) {
        LOG.info("Got event: " + eventData.length());
        return Response.ok().build();
}

and my reading of the docs for custom authentications would suggest that MyCustomProvider.authenticate() should be called before the http function consume() method. This does not happen. It could be that my understanding of the docs is incorrect, but if so it is not at all clear how a custom authenticator should work.

michalvavrik commented 2 months ago

I think this ticket should be repoened. I can't get a CUSTOM lambda security integration to work at all.

I appreciate you have issue with a same title, but I closed it for good reasons, did you read my comment here https://github.com/quarkusio/quarkus/issues/24609#issuecomment-1327826314 ? I think it would be confusing to reopen same issue unless you have exactly same issue.

Would it be alright to open a new issue with reproducer (because your examples are not enough) and link it with this one, please? IMO your questions and examples describe different scenario. Thank you

michalvavrik commented 2 months ago

@brucej72 please also ping me in a new issue, thank you

brucej72 commented 2 months ago

Hi @michalvavrik I think the issue is exactly the same as the original, and the reproducer code originally supplied here will demonstrate the problem. All I did was to add an injected logger to show that the authenticate method wasn't being called.

I don't really follow your point here. I know I can write a completely separate Lambda authenticator for API gateway, but the docs clearly suggest that I don't need to do this: I just need to enable security and them write a CDI bean that implements LambdaIdentityProvider, and then requests to the lambda http function will trigger my customer authenticator to be called:

"For HTTP, the important method to override is LambdaIdentityProvider.authenticate(APIGatewayV2HTTPEvent event). From this you will allocate a SecurityIdentity based on how you want to map security data from APIGatewayV2HTTPEvent"

However, this method is not called either in my code or in the original code supplied by @faskan when running under quarkus dev and hitting the (local) lambda endpoint with a browser or postman.

michalvavrik commented 2 months ago

@brucej72 I have reopened it for you.

Sorry you don't understand my explanation, but I still believe that situation is clear. The identity provider you supply is only invoked when it is authenticable https://github.com/quarkusio/quarkus/blob/main/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java#L71. It will never be authenticable when the authorizer is empty. I even tried it judging by the comment I left here almost 2 years ago. Documentation also seems clear to me, it says: From this you will allocate a SecurityIdentity based on how you want to map security data from AwsProxyRequest So you cannot do mapping if you don't have security data.

Personally I think this is not a bug, but a feature request and you would be better off with a new issue. However I am not engineer responsible for Quarkus Amazon Lambda HTTP and I can be easily wrong. Please consider everything that I wrote as my personal opinion and nothing more.

@patriot1burke please have a look into this, thank you

brucej72 commented 2 months ago

Hi @michalvavrik the point I took away from the docs was that if I have access to the the APIGatewayV2HTTPEvent object (via LambdaIdentityProvider.authenticate(APIGatewayV2HTTPEvent event) I can implement my own auth based on whatever, for example an HMAC scheme. However, the authenticate method is never called, so I can only suggest that this is either a bug or that the docs are incorrect in what they suggest is possible. I was also thinking about the line below the one you linked: https://github.com/quarkusio/quarkus/blob/e1577e6f462b325ca050f6326b5cb7fffab91b10/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java#L72

If I provide my own implementation of LambdaIdentityProvider, would you not expect (event.getRequestContext() != null && event.getRequestContext().getAuthorizer() != null) to be true, and therefore this be authenticabable?

michalvavrik commented 2 months ago

If I provide my own implementation of LambdaIdentityProvider, would you not expect (event.getRequestContext() != null && event.getRequestContext().getAuthorizer() != null) to be true, and therefore this be authenticabable?

No, I would not, because this has nothing to do with your LambdaIdentityProvider, you can read a code and determine why your provider is not called.

Hi @michalvavrik the point I took away from the docs was that if I have access to the the APIGatewayV2HTTPEvent object (via LambdaIdentityProvider.authenticate(APIGatewayV2HTTPEvent event) I can implement my own auth based on whatever, for example an HMAC scheme. However, the authenticate method is never called, so I can only suggest that this is either a bug or that the docs are incorrect in what they suggest is possible.

I don't know how else to explain why is it not called. Let's move on towards your options until this ticket is addressed by someone else.

If you want to make this work until then, here is how to do that:

  1. Implement custom HttpAuthenticationMechanism as documented here https://quarkus.io/version/3.8/guides/security-customization#httpauthenticationmechanism-customization
  2. make sure it has higher priority than LambdaHttpAuthenticationMechanism
  3. access AWS event as is done here https://github.com/quarkusio/quarkus/blob/e1577e6f462b325ca050f6326b5cb7fffab91b10/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java#L47

IMO this should be improved for users to avoid all these steps. I hope this helps.

brucej72 commented 2 months ago

Thank you @michalvavrik this does help a lot. I was not aware that I could implement HttpAuthenticationMechanism and set my implementation to have a higher priority and then get the the APIGatewayV2HTTPEvent object in this way. I think the fault lies in the documentation and perhaps the section here should reference this with an explanation that it is possible to provide a completely custom solution to authenticate the http lambda function in the way you suggest.

michalvavrik commented 1 day ago

ping @patriot1burke