From 3f234a79bb7d017242df9fd88f13cd92ef90a932 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 12 May 2021 16:14:32 -0400 Subject: [PATCH] AWS Lambda HTTP Security Integration sam local docs convert lambda security to auth mechanism default providers add mechanism if empty credential types --- .../src/main/asciidoc/amazon-lambda-http.adoc | 85 ++++++++++++++++ .../amazon-lambda-http/deployment/pom.xml | 4 + .../deployment/AmazonLambdaHttpProcessor.java | 16 ++++ .../deployment/LambdaHttpBuildTimeConfig.java | 14 +++ extensions/amazon-lambda-http/runtime/pom.xml | 4 + .../amazon/lambda/http/CognitoPrincipal.java | 30 ++++++ .../amazon/lambda/http/CustomPrincipal.java | 30 ++++++ .../DefaultLambdaAuthenticationRequest.java | 20 ++++ .../http/DefaultLambdaIdentityProvider.java | 96 +++++++++++++++++++ .../amazon/lambda/http/IAMPrincipal.java | 30 ++++++ .../http/LambdaAuthenticationRequest.java | 17 ++++ .../LambdaHttpAuthenticationMechanism.java | 91 ++++++++++++++++++ .../amazon/lambda/http/LambdaHttpHandler.java | 33 ++++--- .../lambda/http/LambdaIdentityProvider.java | 42 ++++++++ .../amazon-lambda-rest/deployment/pom.xml | 4 + .../deployment/AmazonLambdaHttpProcessor.java | 17 +++- .../deployment/LambdaHttpBuildTimeConfig.java | 14 +++ extensions/amazon-lambda-rest/runtime/pom.xml | 4 + .../amazon/lambda/http/CognitoPrincipal.java | 31 ++++++ .../amazon/lambda/http/CustomPrincipal.java | 30 ++++++ .../DefaultLambdaAuthenticationRequest.java | 19 ++++ .../http/DefaultLambdaIdentityProvider.java | 87 +++++++++++++++++ .../amazon/lambda/http/IAMPrincipal.java | 31 ++++++ .../http/LambdaAuthenticationRequest.java | 16 ++++ .../LambdaHttpAuthenticationMechanism.java | 90 +++++++++++++++++ .../amazon/lambda/http/LambdaHttpHandler.java | 1 + .../lambda/http/LambdaIdentityProvider.java | 43 +++++++++ .../QuarkusIdentityProviderManagerImpl.java | 12 ++- .../runtime/UndertowDeploymentRecorder.java | 4 +- .../runtime/security/HttpAuthenticator.java | 10 +- .../amazon/lambda/CustomSecurityProvider.java | 35 +++++++ .../amazon/lambda/SecurityCheckResource.java | 18 ++++ .../src/main/resources/application.properties | 1 + .../lambda/AmazonLambdaSimpleTestCase.java | 10 ++ .../amazon/lambda/SecurityCheckResource.java | 18 ++++ .../it/amazon/lambda/SecurityCheckVertx.java | 16 ++++ .../it/amazon/lambda/SecurityServlet.java | 19 ++++ .../src/main/resources/application.properties | 1 + .../lambda/AmazonLambdaSimpleTestCase.java | 58 +++++++++++ .../lambda/v1/SecurityCheckResource.java | 18 ++++ .../amazon/lambda/v1/SecurityCheckVertx.java | 16 ++++ .../it/amazon/lambda/v1/SecurityServlet.java | 19 ++++ .../src/main/resources/application.properties | 1 + .../lambda/AmazonLambdaV1SimpleTestCase.java | 56 +++++++++++ 44 files changed, 1183 insertions(+), 28 deletions(-) create mode 100644 extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java create mode 100644 extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java create mode 100644 extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java create mode 100644 extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java create mode 100644 integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java create mode 100644 integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java create mode 100644 integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java create mode 100644 integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java create mode 100644 integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java create mode 100644 integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java create mode 100644 integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java create mode 100644 integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java diff --git a/docs/src/main/asciidoc/amazon-lambda-http.adoc b/docs/src/main/asciidoc/amazon-lambda-http.adoc index 8fcf6ec8e4f9a6..1ab02d8ad79aba 100644 --- a/docs/src/main/asciidoc/amazon-lambda-http.adoc +++ b/docs/src/main/asciidoc/amazon-lambda-http.adoc @@ -276,3 +276,88 @@ public class MyResource { If you are building native images, and want to use https://aws.amazon.com/xray[AWS X-Ray Tracing] with your lambda you will need to include `quarkus-amazon-lambda-xray` as a dependency in your pom. The AWS X-Ray library is not fully compatible with GraalVM so we had to do some integration work to make this work. + +== Security Integration + +When you invoke an HTTP request on the API Gateway, the Gateway turns that HTTP request into a JSON event document that is +forwarded to a Quarkus Lambda. The Quarkus Lambda parses this json and converts in into an internal representation of an HTTP +request that can be consumed by any HTTP framework Quarkus supports (JAX-RS, servlet, Vert.x Web). + +API Gateway supports many different ways to securely invoke on your HTTP endpoints that are backed by Lambda and Quarkus. +By default, Quarkus will automatically parse relevant parts of the https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html[event json document] +and look for security based metadata and register a `java.security.Principal` internally that can be looked up in JAX-RS +by injecting a `javax.ws.rs.core.SecurityContext`, via `HttpServletRequest.getUserPrincipal()` in servlet, and `RouteContext.user()` in Vert.x Web. +If you want more security information, the `Principal` object can be typecast to +a class that will give you more information. + +Here's how its mapped: + +.HTTP `quarkus-amazon-lambda-http` +[options="header"] +|======================= +|Auth Type |Principal Class |Json path of Principal Name +|Cognito JWT |`io.quarkus.amazon.lambda.http.CognitoPrincipal`|`requestContext.authorizer.jwt.claims.cognito:username` +|IAM |`io.quarkus.amazon.lambda.http.IAMPrincipal` |`requestContext.authorizer.iam.userId` +|Custom Lambda |`io.quarkus.amazon.lambda.http.CustomPrincipal` |`requestContext.authorizer.lambda.principalId` + +|======================= + +.REST `quarkus-amazon-lambda-rest` +[options="header"] +|======================= +|Auth Type |Principal Class |Json path of Principal Name +|Cognito |`io.quarkus.amazon.lambda.http.CognitoPrincipal`|`requestContext.authorizer.claims.cognito:username` +|IAM |`io.quarkus.amazon.lambda.http.IAMPrincipal` |`requestContext.identity.user` +|Custom Lambda |`io.quarkus.amazon.lambda.http.CustomPrincipal` |`requestContext.authorizer.principalId` + +|======================= + +== Custom Security Integration + +The default support for AWS security only maps the principal name to Quarkus security +APIs and does nothing to map claims or roles or permissions. You have can full control +how security metadata in the lambda HTTP event is mapped to Quarkus security APIs using +implementations of the `io.quarkus.amazon.lambda.http.LambdaSecurityIdentityProvider` +interface. + +.HTTP `quarkus-amazon-lambda-http` +[source, java] +---- +package io.quarkus.amazon.lambda.http; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(APIGatewayV2HTTPEvent event); +} +---- + +.REST `quarkus-amazon-lambda-rest` +[source, java] +---- +package io.quarkus.amazon.lambda.http; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(AwsProxyRequest event); +} +---- + +To plugin an implementation of one of these interfaces, set the +`quarkus.lambda-http.identity-provider` application.properties value +to the fully qualified class name of your implementation. + +When Quarkus receives the HTTP event from the API Gateway, it will invoke the +`create()` method. The `io.quarkus.security.identity.SecurityIdentity` interface +defines how your security metadata maps to standard Quarkus security APIs. In that +implementation, you can define things like role mappings for your principal. + +== Simple SAM Local Principal + +If you are testing your application with `sam local` you can +hardcode a principal name to use when your application runs by setting +the `QUARKUS_AWS_LAMBDA_FORCE_USER_NAME` environment variable diff --git a/extensions/amazon-lambda-http/deployment/pom.xml b/extensions/amazon-lambda-http/deployment/pom.xml index be40bb1244994f..2d1091906ac49b 100644 --- a/extensions/amazon-lambda-http/deployment/pom.xml +++ b/extensions/amazon-lambda-http/deployment/pom.xml @@ -19,6 +19,10 @@ io.quarkus quarkus-core-deployment + + io.quarkus + quarkus-security-deployment + io.quarkus quarkus-vertx-http-deployment diff --git a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java index 6f5203ac4bbdbc..ea559b4de05046 100644 --- a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java +++ b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java @@ -7,9 +7,12 @@ import io.quarkus.amazon.lambda.deployment.LambdaUtil; import io.quarkus.amazon.lambda.deployment.ProvidedAmazonLambdaHandlerBuildItem; +import io.quarkus.amazon.lambda.http.DefaultLambdaIdentityProvider; +import io.quarkus.amazon.lambda.http.LambdaHttpAuthenticationMechanism; import io.quarkus.amazon.lambda.http.LambdaHttpHandler; import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.LaunchModeBuildItem; @@ -24,6 +27,19 @@ public class AmazonLambdaHttpProcessor { private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class); + @BuildStep + public void setupSecurity(BuildProducer additionalBeans, + LambdaHttpBuildTimeConfig config) { + if (!config.enableSecurity) + return; + + AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder().setUnremovable(); + + builder.addBeanClass(LambdaHttpAuthenticationMechanism.class) + .addBeanClass(DefaultLambdaIdentityProvider.class); + additionalBeans.produce(builder.build()); + } + @BuildStep public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) { return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null; diff --git a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java new file mode 100644 index 00000000000000..ba81e36664ba09 --- /dev/null +++ b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.amazon.lambda.http.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class LambdaHttpBuildTimeConfig { + /** + * Enable security mechanisms to process lambda and AWS based security (i.e. Cognito, IAM) from + * the http event sent from API Gateway + */ + @ConfigItem(defaultValue = "false") + public boolean enableSecurity; +} diff --git a/extensions/amazon-lambda-http/runtime/pom.xml b/extensions/amazon-lambda-http/runtime/pom.xml index 9efa5c2aa7f6c8..96b8fe96c61221 100644 --- a/extensions/amazon-lambda-http/runtime/pom.xml +++ b/extensions/amazon-lambda-http/runtime/pom.xml @@ -20,6 +20,10 @@ io.quarkus quarkus-vertx-http + + io.quarkus + quarkus-security + io.quarkus quarkus-amazon-lambda diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java new file mode 100644 index 00000000000000..32dea02ac75a43 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +/** + * Represents a Cognito JWT used to authenticate request + * + * Will only be allocated if requestContext.authorizer.jwt.claims.cognito:username is set + * in the http event sent by API Gateway + */ +public class CognitoPrincipal implements Principal { + private APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt; + private String name; + + public CognitoPrincipal(APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt) { + this.jwt = jwt; + this.name = jwt.getClaims().get("cognito:username"); + } + + @Override + public String getName() { + return name; + } + + public APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT getClaims() { + return jwt; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java new file mode 100644 index 00000000000000..cbeb661cde8a1b --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +/** + * Represents a custom principal sent by API Gateway i.e. a Lambda authorizer + * + * Will only be allocated if requestContext.authorizer.lambda.principalId is set + * in the http event sent by API Gateway + * + */ +public class CustomPrincipal implements Principal { + private String name; + private Map claims; + + public CustomPrincipal(String name, Map claims) { + this.claims = claims; + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public Map getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java new file mode 100644 index 00000000000000..6247990f569dae --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java @@ -0,0 +1,20 @@ +package io.quarkus.amazon.lambda.http; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.request.BaseAuthenticationRequest; + +/** + * This will execute if and only if there is no identity after invoking a LambdaAuthenticationRequest + */ +final public class DefaultLambdaAuthenticationRequest extends BaseAuthenticationRequest { + private APIGatewayV2HTTPEvent event; + + public DefaultLambdaAuthenticationRequest(APIGatewayV2HTTPEvent event) { + this.event = event; + } + + public APIGatewayV2HTTPEvent getEvent() { + return event; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java new file mode 100644 index 00000000000000..d5f6719f395be8 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java @@ -0,0 +1,96 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +final public class DefaultLambdaIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return DefaultLambdaAuthenticationRequest.class; + } + + @Override + public Uni authenticate(DefaultLambdaAuthenticationRequest request, + AuthenticationRequestContext context) { + APIGatewayV2HTTPEvent event = request.getEvent(); + SecurityIdentity identity = authenticate(event); + if (identity == null) { + return Uni.createFrom().optional(Optional.empty()); + } + return Uni.createFrom().item(identity); + } + + /** + * Create a SecurityIdentity with a principal derived from APIGatewayV2HTTPEvent. + * Looks for Cognito JWT, IAM, or Custom Lambda metadata for principal name + * + * @param event + * @return + */ + public static SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) { + Principal principal = getPrincipal(event); + if (principal == null) { + return null; + } + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); + builder.setPrincipal(principal); + return builder.build(); + } + + protected static Principal getPrincipal(APIGatewayV2HTTPEvent request) { + final Map systemEnvironment = System.getenv(); + final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); + final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null || requestContext.getAuthorizer() == null)) { + final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); + if (forcedUserName != null && !forcedUserName.isEmpty()) { + return new Principal() { + + @Override + public String getName() { + return forcedUserName; + } + + }; + } + } else { + if (requestContext != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer authorizer = requestContext.getAuthorizer(); + if (authorizer != null) { + if (authorizer.getJwt() != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); + final Map claims = jwt.getClaims(); + if (claims != null && claims.containsKey("cognito:username")) { + return new CognitoPrincipal(jwt); + } + } else if (authorizer.getIam() != null) { + if (authorizer.getIam().getUserId() != null) { + return new IAMPrincipal(authorizer.getIam()); + } + } else if (authorizer.getLambda() != null) { + Object tmp = authorizer.getLambda().get("principalId"); + if (tmp != null && tmp instanceof String) { + String username = (String) tmp; + return new CustomPrincipal(username, authorizer.getLambda()); + } + } + } + } + } + return null; + } + +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java new file mode 100644 index 00000000000000..a236dbcca98b13 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +/** + * Used if IAM is used for authentication. + * + * Will only be allocated if requestContext.authorizer.iam.userId is set + * in the http event sent by API Gateway + */ +public class IAMPrincipal implements Principal { + private String name; + private APIGatewayV2HTTPEvent.RequestContext.IAM iam; + + public IAMPrincipal(APIGatewayV2HTTPEvent.RequestContext.IAM iam) { + this.iam = iam; + this.name = iam.getUserId(); + } + + @Override + public String getName() { + return name; + } + + public APIGatewayV2HTTPEvent.RequestContext.IAM getIam() { + return iam; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java new file mode 100644 index 00000000000000..25d18a48a444f1 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java @@ -0,0 +1,17 @@ +package io.quarkus.amazon.lambda.http; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.request.BaseAuthenticationRequest; + +public class LambdaAuthenticationRequest extends BaseAuthenticationRequest { + private APIGatewayV2HTTPEvent event; + + public LambdaAuthenticationRequest(APIGatewayV2HTTPEvent event) { + this.event = event; + } + + public APIGatewayV2HTTPEvent getEvent() { + return event; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java new file mode 100644 index 00000000000000..128b0fd8593411 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java @@ -0,0 +1,91 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class LambdaHttpAuthenticationMechanism implements HttpAuthenticationMechanism { + @Inject + Instance> identityProviders; + + // there is no way in CDI to currently provide a prioritized list of IdentityProvider + // So, what we do here is to try to see if anybody has registered one. If no identity, then + // fire off a request that can only be resolved by the DefaultLambdaIdentityProvider + boolean useDefault; + + @PostConstruct + public void initialize() { + useDefault = !identityProviders.iterator().hasNext(); + } + + @Override + public Uni authenticate(RoutingContext routingContext, IdentityProviderManager identityProviderManager) { + MultiMap qheaders = routingContext.request().headers(); + if (qheaders instanceof QuarkusHttpHeaders) { + Map, Object> contextObjects = ((QuarkusHttpHeaders) qheaders).getContextObjects(); + if (contextObjects.containsKey(APIGatewayV2HTTPEvent.class)) { + APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) contextObjects.get(APIGatewayV2HTTPEvent.class); + if (useDefault) { + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new DefaultLambdaAuthenticationRequest(event), routingContext)); + + } else { + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new LambdaAuthenticationRequest(event), routingContext)); + } + } + } + return Uni.createFrom().optional(Optional.empty()); + } + + @Override + public Uni sendChallenge(RoutingContext context) { + return Uni.createFrom().item(false); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().nullItem(); + } + + static final Set> credentialTypes = new HashSet<>(); + + static { + credentialTypes.add(LambdaAuthenticationRequest.class); + credentialTypes.add(DefaultLambdaAuthenticationRequest.class); + } + + @Override + public Set> getCredentialTypes() { + return credentialTypes; + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return null; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index 90187615ed3e80..94f11b47f07e84 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -169,10 +169,6 @@ private APIGatewayV2HTTPResponse nettyDispatch(InetSocketAddress clientAddress, quarkusHeaders.setContextObject(Context.class, context); quarkusHeaders.setContextObject(APIGatewayV2HTTPEvent.class, request); quarkusHeaders.setContextObject(APIGatewayV2HTTPEvent.RequestContext.class, request.getRequestContext()); - final Principal principal = getPrincipal(request); - if (principal != null) { - quarkusHeaders.setContextObject(Principal.class, principal); - } DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(request.getRequestContext().getHttp().getMethod()), ofNullable(request.getRawQueryString()) .filter(q -> !q.isEmpty()).map(q -> request.getRawPath() + '?' + q).orElse(request.getRawPath()), @@ -242,7 +238,8 @@ private boolean isBinary(String contentType) { private Principal getPrincipal(APIGatewayV2HTTPEvent request) { final Map systemEnvironment = System.getenv(); final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); - if (isSamLocal) { + final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null || requestContext.getAuthorizer() == null)) { final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); if (forcedUserName != null && !forcedUserName.isEmpty()) { log.info("Forcing local user to " + forcedUserName); @@ -256,22 +253,24 @@ public String getName() { }; } } else { - final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); if (requestContext != null) { final APIGatewayV2HTTPEvent.RequestContext.Authorizer authorizer = requestContext.getAuthorizer(); if (authorizer != null) { - final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); - if (jwt != null) { + if (authorizer.getJwt() != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); final Map claims = jwt.getClaims(); - if (claims != null) { - final String jwtUsername = claims.get("cognito:username"); - if (jwtUsername != null && !jwtUsername.isEmpty()) - return new Principal() { - @Override - public String getName() { - return jwtUsername; - } - }; + if (claims != null && claims.containsKey("cognito:username")) { + return new CognitoPrincipal(jwt); + } + } else if (authorizer.getIam() != null) { + if (authorizer.getIam().getUserId() != null) { + return new IAMPrincipal(authorizer.getIam()); + } + } else if (authorizer.getLambda() != null) { + Object tmp = authorizer.getLambda().get("principalId"); + if (tmp != null && tmp instanceof String) { + String username = (String) tmp; + return new CustomPrincipal(username, authorizer.getLambda()); } } } diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java new file mode 100644 index 00000000000000..31c926ef64d139 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java @@ -0,0 +1,42 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.Optional; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +/** + * Helper interface that removes some boilerplate for creating + * an IdentityProvider that processes APIGatewayV2HTTPEvent + */ +public interface LambdaIdentityProvider extends IdentityProvider { + @Override + default public Class getRequestType() { + return LambdaAuthenticationRequest.class; + } + + @Override + default Uni authenticate(LambdaAuthenticationRequest request, AuthenticationRequestContext context) { + APIGatewayV2HTTPEvent event = request.getEvent(); + SecurityIdentity identity = authenticate(event); + if (identity == null) { + return Uni.createFrom().optional(Optional.empty()); + } + return Uni.createFrom().item(identity); + } + + /** + * You must override this method unless you directly override + * IdentityProvider.authenticate + * + * @param event + * @return + */ + default SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) { + throw new IllegalStateException("You must override this method or IdentityProvider.authenticate"); + } +} diff --git a/extensions/amazon-lambda-rest/deployment/pom.xml b/extensions/amazon-lambda-rest/deployment/pom.xml index f8352f7db4ef5f..32362c7132335c 100644 --- a/extensions/amazon-lambda-rest/deployment/pom.xml +++ b/extensions/amazon-lambda-rest/deployment/pom.xml @@ -19,6 +19,10 @@ io.quarkus quarkus-core-deployment + + io.quarkus + quarkus-security-deployment + io.quarkus quarkus-vertx-http-deployment diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java index ab26b36b472d6c..68242ca8e9f806 100644 --- a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java @@ -4,6 +4,8 @@ import io.quarkus.amazon.lambda.deployment.LambdaUtil; import io.quarkus.amazon.lambda.deployment.ProvidedAmazonLambdaHandlerBuildItem; +import io.quarkus.amazon.lambda.http.DefaultLambdaIdentityProvider; +import io.quarkus.amazon.lambda.http.LambdaHttpAuthenticationMechanism; import io.quarkus.amazon.lambda.http.LambdaHttpHandler; import io.quarkus.amazon.lambda.http.model.AlbContext; import io.quarkus.amazon.lambda.http.model.ApiGatewayAuthorizerContext; @@ -15,6 +17,7 @@ import io.quarkus.amazon.lambda.http.model.ErrorModel; import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.LaunchModeBuildItem; @@ -29,6 +32,19 @@ public class AmazonLambdaHttpProcessor { private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class); + @BuildStep + public void setupSecurity(BuildProducer additionalBeans, + LambdaHttpBuildTimeConfig config) { + if (!config.enableSecurity) + return; + + AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder().setUnremovable(); + + builder.addBeanClass(LambdaHttpAuthenticationMechanism.class) + .addBeanClass(DefaultLambdaIdentityProvider.class); + additionalBeans.produce(builder.build()); + } + @BuildStep public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) { return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null; @@ -79,5 +95,4 @@ public void generateScripts(OutputTargetBuildItem target, .replace("${lambdaName}", lambdaName); LambdaUtil.writeFile(target, "sam.native.yaml", output); } - } diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java new file mode 100644 index 00000000000000..ba81e36664ba09 --- /dev/null +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.amazon.lambda.http.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class LambdaHttpBuildTimeConfig { + /** + * Enable security mechanisms to process lambda and AWS based security (i.e. Cognito, IAM) from + * the http event sent from API Gateway + */ + @ConfigItem(defaultValue = "false") + public boolean enableSecurity; +} diff --git a/extensions/amazon-lambda-rest/runtime/pom.xml b/extensions/amazon-lambda-rest/runtime/pom.xml index 8be168ad30bc67..f97aaa8984870d 100644 --- a/extensions/amazon-lambda-rest/runtime/pom.xml +++ b/extensions/amazon-lambda-rest/runtime/pom.xml @@ -19,6 +19,10 @@ io.quarkus quarkus-vertx-http + + io.quarkus + quarkus-security + io.quarkus quarkus-amazon-lambda diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java new file mode 100644 index 00000000000000..d3a22e2ad52f8c --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import io.quarkus.amazon.lambda.http.model.CognitoAuthorizerClaims; + +/** + * Allocated when cognito is used to authenticate user + * + * Will only be allocated if requestContext.authorizer.claims.cognito:username is set + * in the http event sent by API Gateway + * + */ +public class CognitoPrincipal implements Principal { + private CognitoAuthorizerClaims claims; + private String name; + + public CognitoPrincipal(CognitoAuthorizerClaims claims) { + this.claims = claims; + this.name = claims.getUsername(); + } + + @Override + public String getName() { + return name; + } + + public CognitoAuthorizerClaims getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java new file mode 100644 index 00000000000000..1f5bdf8f5a8a3d --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +/** + * Allocated when a custom authorizer (i.e. Lambda) is used to authenticate user + * + * Will only be allocated if requestContext.authorizer.principalId is set + * in the http event sent by API Gateway + * + */ +public class CustomPrincipal implements Principal { + private String name; + private Map claims; + + public CustomPrincipal(String name, Map claims) { + this.claims = claims; + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public Map getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java new file mode 100644 index 00000000000000..c716204b57e357 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaAuthenticationRequest.java @@ -0,0 +1,19 @@ +package io.quarkus.amazon.lambda.http; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; + +/** + * This will execute if and only if there is no identity after invoking a LambdaAuthenticationRequest + */ +final public class DefaultLambdaAuthenticationRequest extends BaseAuthenticationRequest { + private AwsProxyRequest event; + + public DefaultLambdaAuthenticationRequest(AwsProxyRequest event) { + this.event = event; + } + + public AwsProxyRequest getEvent() { + return event; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java new file mode 100644 index 00000000000000..b054073762a678 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaIdentityProvider.java @@ -0,0 +1,87 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +final public class DefaultLambdaIdentityProvider implements IdentityProvider { + + @Override + public Class getRequestType() { + return DefaultLambdaAuthenticationRequest.class; + } + + @Override + public Uni authenticate(DefaultLambdaAuthenticationRequest request, + AuthenticationRequestContext context) { + AwsProxyRequest event = request.getEvent(); + SecurityIdentity identity = authenticate(event); + if (identity == null) { + return Uni.createFrom().optional(Optional.empty()); + } + return Uni.createFrom().item(identity); + } + + /** + * Create a SecurityIdentity with a principal derived from APIGatewayV2HTTPEvent. + * Looks for Cognito JWT, IAM, or Custom Lambda metadata for principal name + * + * @param event + * @return + */ + public static SecurityIdentity authenticate(AwsProxyRequest event) { + Principal principal = getPrincipal(event); + if (principal == null) { + return null; + } + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); + builder.setPrincipal(principal); + return builder.build(); + } + + public static Principal getPrincipal(AwsProxyRequest request) { + final Map systemEnvironment = System.getenv(); + final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); + final AwsProxyRequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null + || (requestContext.getAuthorizer() == null && requestContext.getIdentity() == null))) { + final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); + if (forcedUserName != null && !forcedUserName.isEmpty()) { + return new Principal() { + + @Override + public String getName() { + return forcedUserName; + } + + }; + } + } else { + if (requestContext != null) { + if (requestContext.getIdentity() != null && requestContext.getIdentity().getUser() != null) { + return new IAMPrincipal(requestContext.getIdentity()); + } else if (requestContext.getAuthorizer() != null) { + if (requestContext.getAuthorizer().getClaims() != null) { + return new CognitoPrincipal(requestContext.getAuthorizer().getClaims()); + } else if (requestContext.getAuthorizer().getPrincipalId() != null) { + return new CustomPrincipal(requestContext.getAuthorizer().getPrincipalId(), + requestContext.getAuthorizer().getContextProperties()); + } + } + } + } + return null; + } + +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java new file mode 100644 index 00000000000000..61c88ebb43d1b9 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import io.quarkus.amazon.lambda.http.model.ApiGatewayRequestIdentity; + +/** + * Allocated when IAM is used to authenticate user + * + * Will only be allocated if requestContext.identity.user is set + * in the http event sent by API Gateway + * + */ +public class IAMPrincipal implements Principal { + private String name; + private ApiGatewayRequestIdentity iam; + + public IAMPrincipal(ApiGatewayRequestIdentity identity) { + this.iam = identity; + this.name = identity.getUser(); + } + + @Override + public String getName() { + return name; + } + + public ApiGatewayRequestIdentity getIam() { + return iam; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java new file mode 100644 index 00000000000000..02a14e9191289a --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaAuthenticationRequest.java @@ -0,0 +1,16 @@ +package io.quarkus.amazon.lambda.http; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.request.BaseAuthenticationRequest; + +public class LambdaAuthenticationRequest extends BaseAuthenticationRequest { + private AwsProxyRequest event; + + public LambdaAuthenticationRequest(AwsProxyRequest event) { + this.event = event; + } + + public AwsProxyRequest getEvent() { + return event; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java new file mode 100644 index 00000000000000..38166399fb076f --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpAuthenticationMechanism.java @@ -0,0 +1,90 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class LambdaHttpAuthenticationMechanism implements HttpAuthenticationMechanism { + @Inject + Instance> identityProviders; + + // there is no way in CDI to currently provide a prioritized list of IdentityProvider + // So, what we do here is to try to see if anybody has registered one. If no identity, then + // fire off a request that can only be resolved by the DefaultLambdaIdentityProvider + boolean useDefault; + + @PostConstruct + public void initialize() { + useDefault = !identityProviders.iterator().hasNext(); + } + + @Override + public Uni authenticate(RoutingContext routingContext, IdentityProviderManager identityProviderManager) { + MultiMap qheaders = routingContext.request().headers(); + if (qheaders instanceof QuarkusHttpHeaders) { + Map, Object> contextObjects = ((QuarkusHttpHeaders) qheaders).getContextObjects(); + if (contextObjects.containsKey(AwsProxyRequest.class)) { + AwsProxyRequest event = (AwsProxyRequest) contextObjects.get(AwsProxyRequest.class); + if (useDefault) { + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new DefaultLambdaAuthenticationRequest(event), routingContext)); + + } else { + return identityProviderManager + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new LambdaAuthenticationRequest(event), routingContext)); + } + } + } + return Uni.createFrom().optional(Optional.empty()); + } + + @Override + public Uni sendChallenge(RoutingContext context) { + return Uni.createFrom().item(false); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().nullItem(); + } + + static final Set> credentialTypes = new HashSet<>(); + + static { + credentialTypes.add(LambdaAuthenticationRequest.class); + credentialTypes.add(DefaultLambdaAuthenticationRequest.class); + } + + @Override + public Set> getCredentialTypes() { + return credentialTypes; + } + + @Override + public HttpCredentialTransport getCredentialTransport() { + return null; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index 484264e245e4de..76f36531ecd77b 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -178,6 +178,7 @@ private AwsProxyResponse nettyDispatch(InetSocketAddress clientAddress, AwsProxy QuarkusHttpHeaders quarkusHeaders = new QuarkusHttpHeaders(); quarkusHeaders.setContextObject(Context.class, context); quarkusHeaders.setContextObject(AwsProxyRequestContext.class, request.getRequestContext()); + quarkusHeaders.setContextObject(AwsProxyRequest.class, request); DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(request.getHttpMethod()), path, quarkusHeaders); if (request.getMultiValueHeaders() != null) { //apparently this can be null if no headers are sent diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java new file mode 100644 index 00000000000000..b43cca7c2fc7e0 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaIdentityProvider.java @@ -0,0 +1,43 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.Optional; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +/** + * Helper interface that removes some boilerplate for creating + * an IdentityProvider that processes APIGatewayV2HTTPEvent + */ +public interface LambdaIdentityProvider extends IdentityProvider { + @Override + default public Class getRequestType() { + return LambdaAuthenticationRequest.class; + } + + @Override + default Uni authenticate(LambdaAuthenticationRequest request, AuthenticationRequestContext context) { + AwsProxyRequest event = request.getEvent(); + SecurityIdentity identity = authenticate(event); + if (identity == null) { + return Uni.createFrom().optional(Optional.empty()); + } + return Uni.createFrom().item(identity); + } + + /** + * You must override this method unless you directly override + * IdentityProvider.authenticate + * + * @param event + * @return + */ + default SecurityIdentity authenticate(AwsProxyRequest event) { + throw new IllegalStateException("You must override this method or IdentityProvider.authenticate"); + } +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java index 8f3b64e2115e0c..4df865c0524642 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; @@ -13,7 +14,6 @@ import org.jboss.logging.Logger; import io.quarkus.runtime.BlockingOperationControl; -import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; @@ -109,6 +109,9 @@ private Uni handleSingleProvider(IdentityProvider identityProv return authenticated.flatMap(new Function>() { @Override public Uni apply(SecurityIdentity securityIdentity) { + if (securityIdentity == null) { + return Uni.createFrom().optional(Optional.empty()); + } return handleIdentityFromProvider(0, securityIdentity, blockingRequestContext); } }); @@ -135,9 +138,7 @@ public SecurityIdentity authenticateBlocking(AuthenticationRequest request) { private Uni handleProvider(int pos, List> providers, T request, AuthenticationRequestContext context) { if (pos == providers.size()) { - //we failed to authentication - log.debug("Authentication failed as providers would authenticate the request"); - return Uni.createFrom().failure(new AuthenticationFailedException()); + return Uni.createFrom().optional(Optional.empty()); } IdentityProvider current = providers.get(pos); Uni cs = current.authenticate(request, context) @@ -153,6 +154,9 @@ public Uni apply(SecurityIdentity securityIdentity) { return cs.onItem().transformToUni(new Function>() { @Override public Uni apply(SecurityIdentity securityIdentity) { + if (securityIdentity == null) { + return Uni.createFrom().optional(Optional.empty()); + } return handleIdentityFromProvider(0, securityIdentity, context); } }); diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index dc09f4c24777ad..aa6532a2f53f3b 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -223,7 +223,9 @@ public RuntimeValue createDeployment(String name, Set kn public void handleNotification(SecurityNotification notification) { if (notification.getEventType() == SecurityNotification.EventType.AUTHENTICATED) { QuarkusUndertowAccount account = (QuarkusUndertowAccount) notification.getAccount(); - CDI.current().select(CurrentIdentityAssociation.class).get().setIdentity(account.getSecurityIdentity()); + Instance instance = CDI.current().select(CurrentIdentityAssociation.class); + if (instance.isResolvable()) + instance.get().setIdentity(account.getSecurityIdentity()); } } }); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index 8d8ddead5a1429..ff94916afd5015 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -44,21 +44,21 @@ public HttpAuthenticator(Instance instance, Instance> providers) { List mechanisms = new ArrayList<>(); for (HttpAuthenticationMechanism mechanism : instance) { - boolean notFound = false; + boolean found = false; for (Class mechType : mechanism.getCredentialTypes()) { - boolean found = false; for (IdentityProvider i : providers) { if (i.getRequestType().equals(mechType)) { found = true; break; } } - if (!found) { - notFound = true; + if (found == true) { break; } } - if (!notFound) { + // Add mechanism if there is a provider with matching credential type + // If the mechanism has no credential types, just add it anyways + if (found || mechanism.getCredentialTypes().isEmpty()) { mechanisms.add(mechanism); } } diff --git a/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java new file mode 100644 index 00000000000000..1a6fb190313c6b --- /dev/null +++ b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java @@ -0,0 +1,35 @@ +package io.quarkus.it.amazon.lambda; + +import java.security.Principal; +import java.util.Optional; + +import javax.enterprise.context.ApplicationScoped; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.amazon.lambda.http.LambdaAuthenticationRequest; +import io.quarkus.amazon.lambda.http.LambdaIdentityProvider; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +//@Alternative +//@Priority(1) +public class CustomSecurityProvider implements LambdaIdentityProvider { + public Uni authenticate(LambdaAuthenticationRequest request, AuthenticationRequestContext context) { + APIGatewayV2HTTPEvent event = request.getEvent(); + if (event.getHeaders() == null || !event.getHeaders().containsKey("x-user")) + return Uni.createFrom().optional(Optional.empty()); + Principal principal = new Principal() { + @Override + public String getName() { + return event.getHeaders().get("x-user"); + } + }; + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(); + builder.setPrincipal(principal); + return Uni.createFrom().item(builder.build()); + } +} diff --git a/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java new file mode 100644 index 00000000000000..73ebc7aa50f56f --- /dev/null +++ b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-http-resteasy/src/main/resources/application.properties b/integration-tests/amazon-lambda-http-resteasy/src/main/resources/application.properties index 6156f0357df511..92b4d0d030efee 100644 --- a/integration-tests/amazon-lambda-http-resteasy/src/main/resources/application.properties +++ b/integration-tests/amazon-lambda-http-resteasy/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.lambda.enable-polling-jvm-mode=true +quarkus.lambda-http.enable-security=true quarkus.http.virtual=true diff --git a/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java b/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java index 03f4a55cd48b25..560849c3a3e167 100644 --- a/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java +++ b/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java @@ -17,6 +17,15 @@ @QuarkusTest public class AmazonLambdaSimpleTestCase { + @Test + public void testCustomIDPSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getHeaders().put("x-user", "John"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "John"); + } + @Test public void testContext() throws Exception { APIGatewayV2HTTPEvent request = new APIGatewayV2HTTPEvent(); @@ -49,6 +58,7 @@ private void testGetText(String path) { private APIGatewayV2HTTPEvent request(String path) { APIGatewayV2HTTPEvent request = new APIGatewayV2HTTPEvent(); + request.setHeaders(new HashMap<>()); request.setRawPath(path); request.setRequestContext(new APIGatewayV2HTTPEvent.RequestContext()); request.getRequestContext().setHttp(new APIGatewayV2HTTPEvent.RequestContext.Http()); diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java new file mode 100644 index 00000000000000..73ebc7aa50f56f --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java new file mode 100644 index 00000000000000..3828fc58af6fb9 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java @@ -0,0 +1,16 @@ +package io.quarkus.it.amazon.lambda; + +import static io.quarkus.vertx.web.Route.HttpMethod.GET; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; + +public class SecurityCheckVertx { + @Route(path = "/vertx/security", methods = GET) + void hello(RoutingContext context) { + context.response().headers().set("Content-Type", "text/plain"); + context.response().setStatusCode(200) + .end(((QuarkusHttpUser) context.user()).getSecurityIdentity().getPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java new file mode 100644 index 00000000000000..8926090982cbdb --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java @@ -0,0 +1,19 @@ +package io.quarkus.it.amazon.lambda; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletSecurity", urlPatterns = "/servlet/security") +public class SecurityServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write(req.getUserPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/resources/application.properties b/integration-tests/amazon-lambda-http/src/main/resources/application.properties index f41be18ef30900..d16f99844778da 100644 --- a/integration-tests/amazon-lambda-http/src/main/resources/application.properties +++ b/integration-tests/amazon-lambda-http/src/main/resources/application.properties @@ -1,3 +1,4 @@ quarkus.lambda.enable-polling-jvm-mode=true quarkus.http.virtual=true +quarkus.lambda-http.enable-security=true quarkus.swagger-ui.always-include=true diff --git a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java index afebdd73b5ddb9..1bf83981f24d71 100644 --- a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java +++ b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java @@ -30,6 +30,64 @@ public void testContext() throws Exception { Assertions.assertEquals(out.getStatusCode(), 204); } + @Test + public void testJaxrsCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testJaxrsIAMSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setIam(new APIGatewayV2HTTPEvent.RequestContext.IAM()); + request.getRequestContext().getAuthorizer().getIam().setUserId("Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testJaxrsCustomLambdaSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setLambda(new HashMap<>()); + request.getRequestContext().getAuthorizer().getLambda().put("principalId", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testServletCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/servlet/security"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testVertxCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/vertx/security"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + @Test public void testGetText() throws Exception { testGetText("/vertx/hello"); diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java new file mode 100644 index 00000000000000..32df6f74888485 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda.v1; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java new file mode 100644 index 00000000000000..61e7a7226eb905 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java @@ -0,0 +1,16 @@ +package io.quarkus.it.amazon.lambda.v1; + +import static io.quarkus.vertx.web.Route.HttpMethod.GET; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; + +public class SecurityCheckVertx { + @Route(path = "/vertx/security", methods = GET) + void hello(RoutingContext context) { + context.response().headers().set("Content-Type", "text/plain"); + context.response().setStatusCode(200) + .end(((QuarkusHttpUser) context.user()).getSecurityIdentity().getPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java new file mode 100644 index 00000000000000..06b27d15045433 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java @@ -0,0 +1,19 @@ +package io.quarkus.it.amazon.lambda.v1; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletSecurity", urlPatterns = "/servlet/security") +public class SecurityServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write(req.getUserPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/resources/application.properties b/integration-tests/amazon-lambda-rest/src/main/resources/application.properties index f41be18ef30900..d16f99844778da 100644 --- a/integration-tests/amazon-lambda-rest/src/main/resources/application.properties +++ b/integration-tests/amazon-lambda-rest/src/main/resources/application.properties @@ -1,3 +1,4 @@ quarkus.lambda.enable-polling-jvm-mode=true quarkus.http.virtual=true +quarkus.lambda-http.enable-security=true quarkus.swagger-ui.always-include=true diff --git a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java index e99974e08bad40..b16ceefce0751b 100644 --- a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java +++ b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java @@ -8,9 +8,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import io.quarkus.amazon.lambda.http.model.ApiGatewayAuthorizerContext; +import io.quarkus.amazon.lambda.http.model.ApiGatewayRequestIdentity; import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext; import io.quarkus.amazon.lambda.http.model.AwsProxyResponse; +import io.quarkus.amazon.lambda.http.model.CognitoAuthorizerClaims; import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.amazon.lambda.test.LambdaClient; import io.quarkus.test.junit.QuarkusTest; @@ -27,6 +30,59 @@ public void testContext() throws Exception { Assertions.assertEquals(out.getStatusCode(), 204); } + @Test + public void testJaxrsSecurityIAM() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity()); + request.getRequestContext().getIdentity().setUser("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testServletSecurityIAM() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/servlet/security"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity()); + request.getRequestContext().getIdentity().setUser("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testJaxrsCognitoSecurityContext() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + request.getRequestContext().getAuthorizer().setClaims(new CognitoAuthorizerClaims()); + request.getRequestContext().getAuthorizer().getClaims().setUsername("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testJaxrsCustomLambdaSecurityContext() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + request.getRequestContext().getAuthorizer().setPrincipalId("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + @Test public void testGetText() throws Exception { testGetText("/vertx/hello");