Skip to content

Commit

Permalink
Merge pull request #17192 from patriot1burke/aws-principal
Browse files Browse the repository at this point in the history
AWS Lambda HTTP Security Integration
  • Loading branch information
patriot1burke authored Jun 8, 2021
2 parents 07bcdda + 7ca2fc8 commit e54aaf9
Show file tree
Hide file tree
Showing 43 changed files with 1,280 additions and 54 deletions.
206 changes: 205 additions & 1 deletion docs/src/main/asciidoc/amazon-lambda-http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ For the AWS REST API you can inject the AWS variables `com.amazonaws.services.la
----
import javax.ws.rs.core.Context;
import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext;
import io.quarkus.amazon.lambda.http.model.AwsProxyRequest;
@Path("/myresource")
Expand All @@ -266,7 +267,10 @@ public class MyResource {
public String ctx(@Context com.amazonaws.services.lambda.runtime.Context ctx) { }
@GET
public String req(@Context AwsProxyRequestContext req) { }
public String reqContext(@Context AwsProxyRequestContext req) { }
@GET
public String req(@Context AwsProxyRequest req) { }
}
----
Expand All @@ -276,3 +280,203 @@ 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.
If you enable it, 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.

To enable this security feature, add this to your `application.properties` file:
----
quarkus.lambda-http.enable-security=true
----


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.LambdaIdentityProvider`
interface. By implementing this interface, you can do things like define role mappings for your principal
or publish additional attributes provided by IAM or Cognito or your Custom Lambda security integration.

.HTTP `quarkus-amazon-lambda-http`
[source, java]
----
package io.quarkus.amazon.lambda.http;
/**
* Helper interface that removes some boilerplate for creating
* an IdentityProvider that processes APIGatewayV2HTTPEvent
*/
public interface LambdaIdentityProvider extends IdentityProvider<LambdaAuthenticationRequest> {
@Override
default public Class<LambdaAuthenticationRequest> getRequestType() {
return LambdaAuthenticationRequest.class;
}
@Override
default Uni<SecurityIdentity> 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");
}
}
----

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`

.REST `quarkus-amazon-lambda-rest`
[source, java]
----
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<LambdaAuthenticationRequest> {
...
/**
* 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");
}
}
----

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

Your implemented provider must be a CDI bean. Here's an example:

[source,java]
----
package org.acme;
import java.security.Principal;
import javax.enterprise.context.ApplicationScoped;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import io.quarkus.amazon.lambda.http.LambdaIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
@ApplicationScoped
public class CustomSecurityProvider implements LambdaIdentityProvider {
@Override
public SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) {
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();
}
}
----

Here's the same example, but with the AWS Gateway REST API:

[source,java]
----
package org.acme;
import java.security.Principal;
import javax.enterprise.context.ApplicationScoped;
import io.quarkus.amazon.lambda.http.model.AwsProxyRequest;
import io.quarkus.amazon.lambda.http.LambdaIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
@ApplicationScoped
public class CustomSecurityProvider implements LambdaIdentityProvider {
@Override
public SecurityIdentity authenticate(AwsProxyRequest event) {
if (event.getMultiValueHeaders() == null || !event.getMultiValueHeaders().containsKey("x-user"))
return null;
Principal principal = new QuarkusPrincipal(event.getMultiValueHeaders().getFirst("x-user"));
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
builder.setPrincipal(principal);
return builder.build();
}
}
----

Quarkus should automatically discover this implementation and use it instead of the default implementation
discussed earlier.

== 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
4 changes: 4 additions & 0 deletions extensions/amazon-lambda-http/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-deployment</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +27,19 @@
public class AmazonLambdaHttpProcessor {
private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class);

@BuildStep
public void setupSecurity(BuildProducer<AdditionalBeanBuildItem> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions extensions/amazon-lambda-http/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> claims;

public CustomPrincipal(String name, Map<String, Object> claims) {
this.claims = claims;
this.name = name;
}

@Override
public String getName() {
return name;
}

public Map<String, Object> getClaims() {
return claims;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit e54aaf9

Please sign in to comment.