Skip to content

Commit

Permalink
AWS Lambda HTTP Security Integration
Browse files Browse the repository at this point in the history
sam local docs

convert lambda security to auth mechanism

default providers

add mechanism if empty credential types
  • Loading branch information
patriot1burke committed May 20, 2021
1 parent bce6a9e commit 3f234a7
Show file tree
Hide file tree
Showing 44 changed files with 1,183 additions and 28 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
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<DefaultLambdaAuthenticationRequest> {

@Override
public Class<DefaultLambdaAuthenticationRequest> getRequestType() {
return DefaultLambdaAuthenticationRequest.class;
}

@Override
public Uni<SecurityIdentity> 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<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
@@ -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;
}
}
Loading

0 comments on commit 3f234a7

Please sign in to comment.