Skip to content

Commit

Permalink
AWS Lambda HTTP Security Integration
Browse files Browse the repository at this point in the history
sam local docs
  • Loading branch information
patriot1burke committed May 12, 2021
1 parent 6431d1e commit 5085f15
Show file tree
Hide file tree
Showing 42 changed files with 1,230 additions and 17 deletions.
85 changes: 85 additions & 0 deletions docs/src/main/asciidoc/amazon-lambda-http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.amazon.lambda.http.deployment;

import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import org.jboss.logging.Logger;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
Expand All @@ -8,22 +10,32 @@
import io.quarkus.amazon.lambda.deployment.LambdaUtil;
import io.quarkus.amazon.lambda.deployment.ProvidedAmazonLambdaHandlerBuildItem;
import io.quarkus.amazon.lambda.http.LambdaHttpHandler;
import io.quarkus.amazon.lambda.http.LambdaHttpRecorder;
import io.quarkus.amazon.lambda.http.SecurityIdentityHandler;
import io.quarkus.amazon.lambda.http.model.Headers;
import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem;
import io.vertx.core.file.impl.FileResolver;

public class AmazonLambdaHttpProcessor {
private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class);

@BuildStep
public void addSecurityFilter(BuildProducer<FilterBuildItem> filters) {
filters.produce(new FilterBuildItem(new SecurityIdentityHandler(), FilterBuildItem.AUTHENTICATION + 1));
}

@BuildStep
public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) {
return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null;
Expand Down Expand Up @@ -73,4 +85,14 @@ public void generateScripts(OutputTargetBuildItem target,
LambdaUtil.writeFile(target, "sam.native.yaml", output);
}

@BuildStep()
@Record(STATIC_INIT)
public void setSecurityProvider(LambdaHttpBuildTimeConfig config,
RecorderContext context,
LambdaHttpRecorder recorder) {
if (config.identityProvider.isPresent()) {
recorder.setLambdaSecurityIdentityProvider(context.newInstance(config.identityProvider.get()));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.amazon.lambda.http.deployment;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot
public class LambdaHttpBuildTimeConfig {
/**
* Fully qualified class name of custom io.quarkus.lambda.http.LambdaSecurityIdentityProvider
*/
public Optional<String> identityProvider;
}
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,63 @@
package io.quarkus.amazon.lambda.http;

import java.security.Principal;
import java.util.Map;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;

import io.quarkus.security.identity.SecurityIdentity;

public class DefaultLambdaSecurityIdentityProvider implements LambdaSecurityIdentityProvider {
@Override
public SecurityIdentity create(APIGatewayV2HTTPEvent event) {
Principal principal = getPrincipal(event);
if (principal != null) {
return new LambdaSecurityIdentity(principal);
}
return null;
}

protected Principal getPrincipal(APIGatewayV2HTTPEvent request) {
final Map<String, String> 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<String, String> 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;
}

}
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;

/**
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.quarkus.amazon.lambda.http.model.Headers;
import io.quarkus.netty.runtime.virtual.VirtualClientConnection;
import io.quarkus.netty.runtime.virtual.VirtualResponseHandler;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders;
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;

Expand Down Expand Up @@ -169,9 +170,9 @@ 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);
final SecurityIdentity identity = LambdaHttpRecorder.identityProvider.create(request);
if (identity != null) {
quarkusHeaders.setContextObject(SecurityIdentity.class, identity);
}
DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.valueOf(request.getRequestContext().getHttp().getMethod()), ofNullable(request.getRawQueryString())
Expand Down Expand Up @@ -242,7 +243,8 @@ private boolean isBinary(String contentType) {
private Principal getPrincipal(APIGatewayV2HTTPEvent request) {
final Map<String, String> 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);
Expand All @@ -256,22 +258,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<String, String> 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());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.amazon.lambda.http;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;

@Recorder
public class LambdaHttpRecorder {
static LambdaSecurityIdentityProvider identityProvider = new DefaultLambdaSecurityIdentityProvider();

public void setLambdaSecurityIdentityProvider(RuntimeValue<LambdaSecurityIdentityProvider> provider) {
identityProvider = provider.getValue();
}
}
Loading

0 comments on commit 5085f15

Please sign in to comment.