Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS Lambda HTTP Security Integration #17192

Merged
merged 1 commit into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
patriot1burke marked this conversation as resolved.
Show resolved Hide resolved
|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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this default to true? If lambda is sending a security principal I would assume it is for a reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole mechanism abstraction adds overhead to every request which will exist if the user isn't using AWS security. I don't know yet if users will always use AWS security, so I'd like to keep the default 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like all these custom principal implementations, given that we have io.quarkus.security.identity.SecurityIdentity#getAttributes() that can be used to store any additional information attached to the identity (this applies to all the different ones, not just here).

Copy link
Contributor Author

@patriot1burke patriot1burke Jun 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't agree. Things just don't map one to one to an attribute list. JWT has claims and scopes (which is a set). IAM has a bunch of different nested metadata. Also, we already have a specific type for this stuff in the event, doesn't make any sense to pull AWS specific security metadata into a untyped string keyed hashmap. All this metadata is already unmarshalled too and to just fill up a hashmap with this unmarshalled data is double the work.

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