From acf9bdddcbe03bf0d1347dd0b31a4893ccbd2975 Mon Sep 17 00:00:00 2001 From: Brian Morin Date: Sat, 30 Sep 2023 20:26:23 -0700 Subject: [PATCH 1/3] Proof of concept implementation of Refresh Access Token in Rust --- .../lambda_rust/Cargo.toml | 31 ++ .../lambda_rust/src/main.rs | 257 ++++++++++++++ .../lib/custom_identity_component-stack.ts | 327 ++++++++++-------- CustomIdentityComponent/package.json | 1 + 4 files changed, 468 insertions(+), 148 deletions(-) create mode 100644 CustomIdentityComponent/lambda_rust/Cargo.toml create mode 100644 CustomIdentityComponent/lambda_rust/src/main.rs diff --git a/CustomIdentityComponent/lambda_rust/Cargo.toml b/CustomIdentityComponent/lambda_rust/Cargo.toml new file mode 100644 index 0000000..a4f4d17 --- /dev/null +++ b/CustomIdentityComponent/lambda_rust/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "lambda-http-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +aws-config = "0" +aws-sdk-secretsmanager = "0" +cached = { version ="0", features = ["async"] } +http = "0.2" +jsonwebkey = { version ="0.3", features = ["jwt-convert"] } +jsonwebtoken = "8" +lambda_http = { version = "0.8", default-features = false, features = ["apigw_http"] } +lambda_runtime = "0.8" +metrics = "0.21.1" +metrics_cloudwatch_embedded = { version = "0.4.1", features = ["lambda"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +reqwest-middleware = "0.2" +reqwest-retry = "0.3" +serde = {version = "1.0", features = ["derive"] } +serde_json = "1.0" +time = "0.3" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" \ No newline at end of file diff --git a/CustomIdentityComponent/lambda_rust/src/main.rs b/CustomIdentityComponent/lambda_rust/src/main.rs new file mode 100644 index 0000000..32657f4 --- /dev/null +++ b/CustomIdentityComponent/lambda_rust/src/main.rs @@ -0,0 +1,257 @@ +use cached::proc_macro::{cached, once}; +use lambda_http::{Body, Error, Request, RequestExt, Response}; +use metrics_cloudwatch_embedded::lambda::handler::run_http; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::{debug, info, info_span}; + +/// Input Jwt token claims +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct ClaimsIn { + sub: String, + iss: String, + kid: String, + aud: String, + scope: String, + access_token_scope: Option, + iat: i64, + nbf: i64, + exp: i64, +} + +// Output Jwt token claims with references to save some allocations +#[derive(Debug, Serialize)] +struct ClaimsOut<'a> { + sub: &'a str, + iss: &'a str, + kid: &'a str, + aud: &'a str, + scope: &'a str, + access_token_scope: Option<&'a str>, + iat: i64, + nbf: i64, + exp: i64, +} + +/// Json body of success responses +#[derive(Debug, Serialize)] +struct ResponsePayload<'a> { + user_id: &'a str, + auth_token: &'a str, + refresh_token: &'a str, + auth_token_expires_in: i64, + refresh_token_expires_in: i64, +} + +fn generate_response(code: u16, body: &str) -> Response { + Response::builder() + .status(code) + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Credentials", "true") + .body(body.into()) + .expect("failed to generate response") +} + +#[cached] +/// get our (cached) aws configuration +async fn get_aws_config() -> Arc { + Arc::new(aws_config::load_from_env().await) +} + +#[cached(time = 900)] +/// get our private kid and key from secrects manager, panic on failure +async fn get_private_key() -> (Arc, Arc) { + info!("refreshing private key from Secrets Manager"); + + let aws_config = get_aws_config().await; + let secrets_client = aws_sdk_secretsmanager::Client::new(&aws_config); + + let jwk: jsonwebkey::JsonWebKey = secrets_client + .get_secret_value() + .secret_id(std::env::var("SECRET_KEY_ID").unwrap()) + .send() + .await + .expect("failed to get SECRET_KEY_ID") + .secret_string() + .expect("SECRET_KEY_ID is blank") + .to_string() + .parse() + .expect("private key is not a valid jwk"); + + ( + Arc::new(jwk.key_id.unwrap()), + Arc::new(jsonwebtoken::EncodingKey::from_rsa_pem(jwk.key.to_pem().as_bytes()).unwrap()), + ) +} + +#[once(time = 900)] +/// get the json web keyset for our issuer, panic on failure +async fn get_keyset(issuer: &str) -> Arc> { + info!("Refreshing json web keyset"); + + use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; + + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + let jwks = client + .get(format!("{issuer}/.well-known/jwks.json")) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + let mut dict = HashMap::new(); + for jwk in jwks.keys { + if let (Some(key_id), jsonwebtoken::jwk::AlgorithmParameters::RSA(rsa)) = + (jwk.common.key_id, &jwk.algorithm) + { + dict.insert( + key_id, + jsonwebtoken::DecodingKey::from_rsa_components(&rsa.n, &rsa.e).unwrap(), + ); + } + } + + if dict.is_empty() { + panic!("jwks has no valid keys"); + } + + Arc::new(dict) +} + +async fn process_token(issuer: &str, refresh_token: &str) -> Result, Error> { + let header = jsonwebtoken::decode_header(refresh_token)?; + let kid = header.kid.ok_or("kid missing from jwt header")?; + + let jks = get_keyset(issuer).await; + let public_key = jks.get(&kid).ok_or("kid not in jks")?; + + let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256); + validation.set_audience(&["refresh"]); + validation.set_issuer(&[issuer]); + + let jwt = jsonwebtoken::decode::(refresh_token, public_key, &validation)?; + debug!("jwt = {jwt:?}"); + + let user_id = jwt.claims.sub.as_str(); + let access_token_scope = &jwt + .claims + .access_token_scope + .ok_or("missing access_token_scope claim")?; + let access_token_duration_sec = 15 * 60; + let existing_exp_value = jwt.claims.exp; + + let (private_kid, private_key) = get_private_key().await; + + // Build a new header with the latest kid + let mut new_header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256); + new_header.kid = Some(private_kid.to_string()); + + let now = time::OffsetDateTime::now_utc().unix_timestamp(); + + // Build a new refresh token + let refresh_claims = ClaimsOut { + sub: user_id, + iss: issuer, + kid: &private_kid, + aud: "refresh", + scope: "refresh", + access_token_scope: Some(access_token_scope), + iat: now, + nbf: now, + exp: existing_exp_value, + }; + let refresh_token = jsonwebtoken::encode(&new_header, &refresh_claims, &private_key)?; + + // Build a new access token + let access_claims = ClaimsOut { + sub: user_id, + iss: issuer, + kid: &private_kid, + aud: "gamebackend", + scope: access_token_scope, + access_token_scope: None, + iat: now, + nbf: now, + exp: now + access_token_duration_sec, + }; + let access_token = jsonwebtoken::encode(&new_header, &access_claims, &private_key)?; + + let response_payload = ResponsePayload { + user_id, + auth_token: &access_token, + auth_token_expires_in: access_token_duration_sec, + refresh_token: &refresh_token, + refresh_token_expires_in: existing_exp_value - now, + }; + + Ok(generate_response( + 200, + &serde_json::to_string(&response_payload)?, + )) +} + +async fn function_handler(issuer: &str, request: Request) -> Result, Error> { + // Get the refresh_token from the query string + let query = request.query_string_parameters(); + let refresh_token = query.first("refresh_token"); + + match refresh_token { + None => { + metrics::increment_counter!("deny", "reason" => "No refresh token provided"); + Ok(generate_response(401, "Error: No refresh token provided")) + } + Some(refresh_token) => match process_token(issuer, refresh_token).await { + Ok(response) => { + metrics::increment_counter!("allow"); + Ok(response) + } + Err(e) => { + // Record the details but don't give the remote client specifics + metrics::increment_counter!("deny", "reason" => e.to_string()); + Ok(generate_response( + 401, + "Error: Failed to validate refresh token", + )) + } + }, + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .json() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with_target(false) + .with_current_span(false) + .without_time() + .init(); + + let issuer = std::env::var("ISSUER_URL").unwrap(); + + let metrics = metrics_cloudwatch_embedded::Builder::new() + .cloudwatch_namespace(std::env::var("POWERTOOLS_METRICS_NAMESPACE").unwrap()) + .with_dimension("service", std::env::var("POWERTOOLS_SERVICE_NAME").unwrap()) + .with_dimension( + "function", + std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap(), + ) + .lambda_cold_start_span(info_span!("cold start").entered()) + .lambda_cold_start_metric("ColdStart") + .with_lambda_request_id("requestId") + .init() + .unwrap(); + + run_http(metrics, |request: Request| { + function_handler(&issuer, request) + }) + .await +} diff --git a/CustomIdentityComponent/lib/custom_identity_component-stack.ts b/CustomIdentityComponent/lib/custom_identity_component-stack.ts index d6b933d..3ebcb8e 100644 --- a/CustomIdentityComponent/lib/custom_identity_component-stack.ts +++ b/CustomIdentityComponent/lib/custom_identity_component-stack.ts @@ -3,11 +3,12 @@ import { Stack, StackProps, CfnOutput, Duration } from 'aws-cdk-lib'; import { Construct } from 'constructs'; +import { RustFunction } from 'cargo-lambda-cdk'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; -import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as apigw from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as events from 'aws-cdk-lib/aws-events'; @@ -42,7 +43,7 @@ export class CustomIdentityComponentStack extends Stack { // The shared policy for basic Lambda access needs for logging. This is similar to the managed Lambda Execution Policy const lambdaBasicPolicy = new iam.PolicyStatement({ - actions: ['logs:CreateLogGroup','logs:CreateLogStream','logs:PutLogEvents'], + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], resources: ['*'], }); @@ -74,7 +75,7 @@ export class CustomIdentityComponentStack extends Stack { // Define a CloudFront distribution for the issuer data const distribution = new cloudfront.Distribution(this, 'IssuerEndpoint', { - defaultBehavior: { origin: new origins.S3Origin(issuer_bucket), cachePolicy: myCachePolicy}, + defaultBehavior: { origin: new origins.S3Origin(issuer_bucket), cachePolicy: myCachePolicy }, enableLogging: true, minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, logBucket: loggingBucket @@ -84,7 +85,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Issuer endpoint used by customer backend components for validation JWT:s - new CfnOutput(this, 'IssuerEndpointUrl', { value: "https://"+distribution.domainName }); + new CfnOutput(this, 'IssuerEndpointUrl', { value: "https://" + distribution.domainName }); // Define a secrets manager secret with name jwk_private_key const secret = new secretsmanager.Secret(this, 'JWKPrivateKeySecret'); @@ -113,7 +114,7 @@ export class CustomIdentityComponentStack extends Stack { memorySize: 2048, environment: { "ISSUER_BUCKET": issuer_bucket.bucketName, - "ISSUER_ENDPOINT": "https://"+distribution.domainName, + "ISSUER_ENDPOINT": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -144,70 +145,70 @@ export class CustomIdentityComponentStack extends Stack { }); // Define a Web Application Firewall with the standard AWS provided rule set - const cfnWebACLManaged = new wafv2.CfnWebACL(this,'CustomIdentityWebACL',{ - defaultAction: { - allow: {} - }, - scope: 'REGIONAL', - visibilityConfig: { - cloudWatchMetricsEnabled: true, - metricName:'MetricForWebACLCDK', - sampledRequestsEnabled: true, - }, - name:'CustomIdentityWebACL', - rules: [{ - name: 'ManagedWafRules', - priority: 0, - statement: { - managedRuleGroupStatement: { - name:'AWSManagedRulesCommonRuleSet', // The standard rule set provided by AWS: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html - vendorName:'AWS' - } - }, - visibilityConfig: { - cloudWatchMetricsEnabled: true, - metricName:'MetricForWebACLCDK-ManagedRules', - sampledRequestsEnabled: true, - }, - overrideAction: { - none: {} - }, - }] - }); - - const cfnWebACLRateLimit = new wafv2.CfnWebACL(this,'CustomIdentityWebACLRateLimit',{ + const cfnWebACLManaged = new wafv2.CfnWebACL(this, 'CustomIdentityWebACL', { defaultAction: { allow: {} }, scope: 'REGIONAL', visibilityConfig: { cloudWatchMetricsEnabled: true, - metricName:'MetricForWebACLCDKRateLimit', + metricName: 'MetricForWebACLCDK', sampledRequestsEnabled: true, }, - name:'CustomIdentityWebACLRateLimit', - rules: [ - // Add rate limiting rule to allow 3.33 TPS from a single IP (1000 per 5 minutes) - { - name: 'RateLimitingRule', - priority: 1, - action: { - block: {} - }, + name: 'CustomIdentityWebACL', + rules: [{ + name: 'ManagedWafRules', + priority: 0, statement: { - rateBasedStatement: { - limit: 1000, - aggregateKeyType: 'IP' + managedRuleGroupStatement: { + name: 'AWSManagedRulesCommonRuleSet', // The standard rule set provided by AWS: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html + vendorName: 'AWS' } }, visibilityConfig: { cloudWatchMetricsEnabled: true, - metricName:'MetricForWebACLCDK-RateLimiting', + metricName: 'MetricForWebACLCDK-ManagedRules', sampledRequestsEnabled: true, - } + }, + overrideAction: { + none: {} + }, }] }); + const cfnWebACLRateLimit = new wafv2.CfnWebACL(this, 'CustomIdentityWebACLRateLimit', { + defaultAction: { + allow: {} + }, + scope: 'REGIONAL', + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: 'MetricForWebACLCDKRateLimit', + sampledRequestsEnabled: true, + }, + name: 'CustomIdentityWebACLRateLimit', + rules: [ + // Add rate limiting rule to allow 3.33 TPS from a single IP (1000 per 5 minutes) + { + name: 'RateLimitingRule', + priority: 1, + action: { + block: {} + }, + statement: { + rateBasedStatement: { + limit: 1000, + aggregateKeyType: 'IP' + } + }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: 'MetricForWebACLCDK-RateLimiting', + sampledRequestsEnabled: true, + } + }] + }); + // Define an API Gateway for the authentication component public endpoint const logGroup = new logs.LogGroup(this, "CustomIdentityAPiAccessLogs"); const api_gateway = new apigw.RestApi(this, 'ApiGateway', { @@ -216,27 +217,27 @@ export class CustomIdentityComponentStack extends Stack { deployOptions: { accessLogDestination: new apigw.LogGroupLogDestination(logGroup), accessLogFormat: apigw.AccessLogFormat.clf(), - loggingLevel : MethodLoggingLevel.ERROR, + loggingLevel: MethodLoggingLevel.ERROR, tracingEnabled: true, stageName: 'prod', } }); // cdk-nag suppression for the API Gateway default logs access NagSuppressions.addResourceSuppressions( - api_gateway,[{ - id: 'AwsSolutions-IAM4', - reason: "We are using the default CW Logs access of API Gateway", - },],true); + api_gateway, [{ + id: 'AwsSolutions-IAM4', + reason: "We are using the default CW Logs access of API Gateway", + },], true); // Attach the Web Application Firewall with the standard AWS provided rule set - new wafv2.CfnWebACLAssociation(this,'ApiGatewayWebACLAssociation',{ + new wafv2.CfnWebACLAssociation(this, 'ApiGatewayWebACLAssociation', { resourceArn: api_gateway.deploymentStage.stageArn, - webAclArn:cfnWebACLManaged.attrArn, + webAclArn: cfnWebACLManaged.attrArn, }); // Attach the WAF with the rate limit rules - new wafv2.CfnWebACLAssociation(this,'ApiGatewayWebACLAssociationRateLimit',{ + new wafv2.CfnWebACLAssociation(this, 'ApiGatewayWebACLAssociationRateLimit', { resourceArn: api_gateway.deploymentStage.stageArn, - webAclArn:cfnWebACLRateLimit.attrArn, + webAclArn: cfnWebACLRateLimit.attrArn, }); // Request validator for the API @@ -268,7 +269,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://"+distribution.domainName, + "ISSUER_URL": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -283,7 +284,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-as-guest').addMethod('GET', new apigw.LambdaIntegration(login_as_guest_function),{ + api_gateway.root.addResource('login-as-guest').addMethod('GET', new apigw.LambdaIntegration(login_as_guest_function), { requestParameters: { 'method.request.querystring.user_id': false, 'method.request.querystring.guest_secret': false, @@ -316,7 +317,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://"+distribution.domainName, + "ISSUER_URL": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -329,9 +330,39 @@ export class CustomIdentityComponentStack extends Stack { NagSuppressions.addResourceSuppressions(refresh_access_token_function_role, [ { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } ], true); - - // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('refresh-access-token').addMethod('GET', new apigw.LambdaIntegration(refresh_access_token_function),{ + + // Map refresh_access_token_function to the api_gateway GET requeste refresh_access_token_function + api_gateway.root.addResource('refresh-access-token').addMethod('GET', new apigw.LambdaIntegration(refresh_access_token_function), { + requestParameters: { + 'method.request.querystring.refresh_token': true + }, + requestValidator: requestValidator + }); + + const refresh_access_token_rust_function = new RustFunction(this, 'RefreshAccessToken_Rust', { + role: refresh_access_token_function_role, + manifestPath: 'lambda_rust/Cargo.toml', + architecture: lambda.Architecture.ARM_64, + bundling: { + forcedDockerBundling: true, + }, + timeout: Duration.seconds(15), + tracing: lambda.Tracing.ACTIVE, + memorySize: 256, + environment: { + "ISSUER_URL": "https://" + distribution.domainName, + "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", + "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", + "RUST_LOG": "info", + "SECRET_KEY_ID": secret.secretName, + "USER_TABLE": user_table.tableName + } + }); + secret.grantRead(refresh_access_token_rust_function); + user_table.grantReadWriteData(refresh_access_token_rust_function); + + // Map refresh_access_token_function to the api_gateway GET requeste refresh_access_token_function + api_gateway.root.addResource('refresh-access-token-rust').addMethod('GET', new apigw.LambdaIntegration(refresh_access_token_rust_function), { requestParameters: { 'method.request.querystring.refresh_token': true }, @@ -342,93 +373,93 @@ export class CustomIdentityComponentStack extends Stack { new CfnOutput(this, 'LoginEndpoint', { value: api_gateway.url }); // If Apple ID App ID Defined, add a DynamoDB table and Lambda function for Apple login - if(props.appleIdAppId != "") { - this.setupAppleIdLogin(props.appleIdAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if (props.appleIdAppId != "") { + this.setupAppleIdLogin(props.appleIdAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Steam App ID defined, add a DynamoDB table and Lambda function for Steam login - if(props.steamAppId != "") { - this.setupSteamLogin(props.steamAppId, props.steamWebApiKeySecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if (props.steamAppId != "") { + this.setupSteamLogin(props.steamAppId, props.steamWebApiKeySecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Google Play App ID defined, add a DynamoDB table and Lambda function for Google Play login - if(props.googlePlayClientId != "") { - this.setupGooglePlayLogin(props.googlePlayClientId, props.googlePlayAppId, props.googlePlayClientSecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if (props.googlePlayClientId != "") { + this.setupGooglePlayLogin(props.googlePlayClientId, props.googlePlayAppId, props.googlePlayClientSecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Facebook App ID defined, add a DynamoDB table and Lambda function for Facebook login - if(props.facebookAppId != "") { - this.setupFacebookLogin(props.facebookAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if (props.facebookAppId != "") { + this.setupFacebookLogin(props.facebookAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } } ///// *** IDENTITY PROVIDER SPECIFIC RESOURECE **** ////// // Sets up Lambda endpoint and DynamoDB table for Apple ID Login - setupAppleIdLogin(appId : string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - - // Define a DynamoDB table for AppleIdUsers - const appleIdUserTable = new dynamodb.Table(this, 'AppleIdUserTable', { - partitionKey: { - name: 'AppleId', - type: dynamodb.AttributeType.STRING - }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - pointInTimeRecovery: true - }); - - // Lambda function for Apple Id login - const loginWithAppleIdFunctionRole = new iam.Role(this, 'LoginWithAppleIdFunctionRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - }); - loginWithAppleIdFunctionRole.addToPolicy(lambdaBasicPolicy); - const loginWithAppleIdFunction = new lambda.Function(this, 'LoginWithAppleId', { - role: loginWithAppleIdFunctionRole, - code: lambda.Code.fromAsset("lambda", { - bundling: { - image: lambda.Runtime.PYTHON_3_11.bundlingImage, - command: [ - 'bash', '-c', - 'pip install --platform manylinux2014_x86_64 --only-binary=:all: -r requirements.txt -t /asset-output && cp -ru . /asset-output' - ], + setupAppleIdLogin(appId: string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + + // Define a DynamoDB table for AppleIdUsers + const appleIdUserTable = new dynamodb.Table(this, 'AppleIdUserTable', { + partitionKey: { + name: 'AppleId', + type: dynamodb.AttributeType.STRING + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + pointInTimeRecovery: true + }); + + // Lambda function for Apple Id login + const loginWithAppleIdFunctionRole = new iam.Role(this, 'LoginWithAppleIdFunctionRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + loginWithAppleIdFunctionRole.addToPolicy(lambdaBasicPolicy); + const loginWithAppleIdFunction = new lambda.Function(this, 'LoginWithAppleId', { + role: loginWithAppleIdFunctionRole, + code: lambda.Code.fromAsset("lambda", { + bundling: { + image: lambda.Runtime.PYTHON_3_11.bundlingImage, + command: [ + 'bash', '-c', + 'pip install --platform manylinux2014_x86_64 --only-binary=:all: -r requirements.txt -t /asset-output && cp -ru . /asset-output' + ], },}), - runtime: lambda.Runtime.PYTHON_3_11, - handler: 'login_with_apple_id.lambda_handler', - timeout: Duration.seconds(15), - tracing: lambda.Tracing.ACTIVE, - memorySize: 2048, - environment: { - "ISSUER_URL": "https://"+distribution.domainName, - "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", - "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", - "SECRET_KEY_ID": secret.secretName, - "USER_TABLE": user_table.tableName, - "APPLE_APP_ID": appId, - "APPLE_ID_USER_TABLE": appleIdUserTable.tableName - } - }); - secret.grantRead(loginWithAppleIdFunction); - user_table.grantReadWriteData(loginWithAppleIdFunction); - appleIdUserTable.grantReadWriteData(loginWithAppleIdFunction); - - NagSuppressions.addResourceSuppressions(loginWithAppleIdFunctionRole, [ - { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } - ], true); - - // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-apple-id').addMethod('GET', new apigw.LambdaIntegration(loginWithAppleIdFunction),{ - requestParameters: { - 'method.request.querystring.apple_auth_token': true, - 'method.request.querystring.auth_token': false, - 'method.request.querystring.link_to_existing_user': false - }, - requestValidator: requestValidator - }); + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'login_with_apple_id.lambda_handler', + timeout: Duration.seconds(15), + tracing: lambda.Tracing.ACTIVE, + memorySize: 2048, + environment: { + "ISSUER_URL": "https://" + distribution.domainName, + "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", + "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", + "SECRET_KEY_ID": secret.secretName, + "USER_TABLE": user_table.tableName, + "APPLE_APP_ID": appId, + "APPLE_ID_USER_TABLE": appleIdUserTable.tableName + } + }); + secret.grantRead(loginWithAppleIdFunction); + user_table.grantReadWriteData(loginWithAppleIdFunction); + appleIdUserTable.grantReadWriteData(loginWithAppleIdFunction); + + NagSuppressions.addResourceSuppressions(loginWithAppleIdFunctionRole, [ + { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } + ], true); + + // Map login_as_guest_function to the api_gateway GET requeste login_as_guest + api_gateway.root.addResource('login-with-apple-id').addMethod('GET', new apigw.LambdaIntegration(loginWithAppleIdFunction), { + requestParameters: { + 'method.request.querystring.apple_auth_token': true, + 'method.request.querystring.auth_token': false, + 'method.request.querystring.link_to_existing_user': false + }, + requestValidator: requestValidator + }); } // Sets up Lambda endpoint and DynamoDB table for Steam ID Login setupSteamLogin(appId: string, steamWebApiKeySecretArn: string, privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - + // Define a DynamoDB table for Steam Users const steamIdUserTable = new dynamodb.Table(this, 'SteamUserTable', { partitionKey: { @@ -460,7 +491,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://"+distribution.domainName, + "ISSUER_URL": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": privateKeySecret.secretName, @@ -486,7 +517,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-steam').addMethod('GET', new apigw.LambdaIntegration(loginWithSteamIdFunction),{ + api_gateway.root.addResource('login-with-steam').addMethod('GET', new apigw.LambdaIntegration(loginWithSteamIdFunction), { requestParameters: { 'method.request.querystring.steam_auth_token': true, 'method.request.querystring.auth_token': false, @@ -498,8 +529,8 @@ export class CustomIdentityComponentStack extends Stack { // Sets up Lambda endpoint and DynamoDB table for Google Play Login setupGooglePlayLogin(googlePlayClientId: string, googlePlayAppId: string, googlePlayClientSecretArn: string, - privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, - distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, + distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { // Define a DynamoDB table for Google Play const googlePlayUserTable = new dynamodb.Table(this, 'GooglePlayUserTable', { @@ -517,7 +548,7 @@ export class CustomIdentityComponentStack extends Stack { }); loginWithGooglePlayFunctionRole.addToPolicy(lambdaBasicPolicy); const loginWithGooglePlayFunction = new lambda.Function(this, 'LoginWithGooglePlay', { - role: loginWithGooglePlayFunctionRole, + role: loginWithGooglePlayFunctionRole, code: lambda.Code.fromAsset("lambda", { bundling: { image: lambda.Runtime.PYTHON_3_11.bundlingImage, @@ -532,7 +563,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://"+distribution.domainName, + "ISSUER_URL": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": privateKeySecret.secretName, @@ -559,7 +590,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-google-play').addMethod('GET', new apigw.LambdaIntegration(loginWithGooglePlayFunction),{ + api_gateway.root.addResource('login-with-google-play').addMethod('GET', new apigw.LambdaIntegration(loginWithGooglePlayFunction), { requestParameters: { 'method.request.querystring.google_play_auth_token': true, 'method.request.querystring.auth_token': false, @@ -569,9 +600,9 @@ export class CustomIdentityComponentStack extends Stack { }); } - // Sets up Lambda endpoint and DynamoDB table for Facebook Login - setupFacebookLogin(appId : string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - + // Sets up Lambda endpoint and DynamoDB table for Facebook Login + setupFacebookLogin(appId: string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + // Define a DynamoDB table for Facebook Users const facebookUserTable = new dynamodb.Table(this, 'FacebookUserTable', { partitionKey: { @@ -603,12 +634,12 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://"+distribution.domainName, + "ISSUER_URL": "https://" + distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, "USER_TABLE": user_table.tableName, - "FACEBOOK_APP_ID" : appId, + "FACEBOOK_APP_ID": appId, "FACEBOOK_USER_TABLE": facebookUserTable.tableName } }); @@ -621,7 +652,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-facebook').addMethod('GET', new apigw.LambdaIntegration(loginWithFacebookFunction),{ + api_gateway.root.addResource('login-with-facebook').addMethod('GET', new apigw.LambdaIntegration(loginWithFacebookFunction), { requestParameters: { 'method.request.querystring.facebook_access_token': true, 'method.request.querystring.facebook_user_id': true, @@ -630,5 +661,5 @@ export class CustomIdentityComponentStack extends Stack { }, requestValidator: requestValidator }); -} + } }; \ No newline at end of file diff --git a/CustomIdentityComponent/package.json b/CustomIdentityComponent/package.json index 458e93d..16d5f88 100644 --- a/CustomIdentityComponent/package.json +++ b/CustomIdentityComponent/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "aws-cdk-lib": "^2.97.0", + "cargo-lambda-cdk": "^0.0.16", "cdk": "^2.81.0-alpha.0", "cdk-nag": "^2.27.24", "constructs": "^10.0.0", From a94748cec3f010fb30ddf802c6f6a032fc3a4bdf Mon Sep 17 00:00:00 2001 From: Brian Morin Date: Sat, 30 Sep 2023 20:40:37 -0700 Subject: [PATCH 2/3] revert whitespace changes to lib\custom_identity_component_stack.ts' --- .../lib/custom_identity_component-stack.ts | 292 +++++++++--------- 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/CustomIdentityComponent/lib/custom_identity_component-stack.ts b/CustomIdentityComponent/lib/custom_identity_component-stack.ts index 3ebcb8e..4e91bbe 100644 --- a/CustomIdentityComponent/lib/custom_identity_component-stack.ts +++ b/CustomIdentityComponent/lib/custom_identity_component-stack.ts @@ -8,7 +8,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; -import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as apigw from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as events from 'aws-cdk-lib/aws-events'; @@ -43,7 +43,7 @@ export class CustomIdentityComponentStack extends Stack { // The shared policy for basic Lambda access needs for logging. This is similar to the managed Lambda Execution Policy const lambdaBasicPolicy = new iam.PolicyStatement({ - actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + actions: ['logs:CreateLogGroup','logs:CreateLogStream','logs:PutLogEvents'], resources: ['*'], }); @@ -75,7 +75,7 @@ export class CustomIdentityComponentStack extends Stack { // Define a CloudFront distribution for the issuer data const distribution = new cloudfront.Distribution(this, 'IssuerEndpoint', { - defaultBehavior: { origin: new origins.S3Origin(issuer_bucket), cachePolicy: myCachePolicy }, + defaultBehavior: { origin: new origins.S3Origin(issuer_bucket), cachePolicy: myCachePolicy}, enableLogging: true, minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, logBucket: loggingBucket @@ -85,7 +85,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Issuer endpoint used by customer backend components for validation JWT:s - new CfnOutput(this, 'IssuerEndpointUrl', { value: "https://" + distribution.domainName }); + new CfnOutput(this, 'IssuerEndpointUrl', { value: "https://"+distribution.domainName }); // Define a secrets manager secret with name jwk_private_key const secret = new secretsmanager.Secret(this, 'JWKPrivateKeySecret'); @@ -114,7 +114,7 @@ export class CustomIdentityComponentStack extends Stack { memorySize: 2048, environment: { "ISSUER_BUCKET": issuer_bucket.bucketName, - "ISSUER_ENDPOINT": "https://" + distribution.domainName, + "ISSUER_ENDPOINT": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -145,70 +145,70 @@ export class CustomIdentityComponentStack extends Stack { }); // Define a Web Application Firewall with the standard AWS provided rule set - const cfnWebACLManaged = new wafv2.CfnWebACL(this, 'CustomIdentityWebACL', { + const cfnWebACLManaged = new wafv2.CfnWebACL(this,'CustomIdentityWebACL',{ + defaultAction: { + allow: {} + }, + scope: 'REGIONAL', + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName:'MetricForWebACLCDK', + sampledRequestsEnabled: true, + }, + name:'CustomIdentityWebACL', + rules: [{ + name: 'ManagedWafRules', + priority: 0, + statement: { + managedRuleGroupStatement: { + name:'AWSManagedRulesCommonRuleSet', // The standard rule set provided by AWS: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html + vendorName:'AWS' + } + }, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName:'MetricForWebACLCDK-ManagedRules', + sampledRequestsEnabled: true, + }, + overrideAction: { + none: {} + }, + }] + }); + + const cfnWebACLRateLimit = new wafv2.CfnWebACL(this,'CustomIdentityWebACLRateLimit',{ defaultAction: { allow: {} }, scope: 'REGIONAL', visibilityConfig: { cloudWatchMetricsEnabled: true, - metricName: 'MetricForWebACLCDK', + metricName:'MetricForWebACLCDKRateLimit', sampledRequestsEnabled: true, }, - name: 'CustomIdentityWebACL', - rules: [{ - name: 'ManagedWafRules', - priority: 0, + name:'CustomIdentityWebACLRateLimit', + rules: [ + // Add rate limiting rule to allow 3.33 TPS from a single IP (1000 per 5 minutes) + { + name: 'RateLimitingRule', + priority: 1, + action: { + block: {} + }, statement: { - managedRuleGroupStatement: { - name: 'AWSManagedRulesCommonRuleSet', // The standard rule set provided by AWS: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html - vendorName: 'AWS' + rateBasedStatement: { + limit: 1000, + aggregateKeyType: 'IP' } }, visibilityConfig: { cloudWatchMetricsEnabled: true, - metricName: 'MetricForWebACLCDK-ManagedRules', + metricName:'MetricForWebACLCDK-RateLimiting', sampledRequestsEnabled: true, - }, - overrideAction: { - none: {} - }, + } }] }); - const cfnWebACLRateLimit = new wafv2.CfnWebACL(this, 'CustomIdentityWebACLRateLimit', { - defaultAction: { - allow: {} - }, - scope: 'REGIONAL', - visibilityConfig: { - cloudWatchMetricsEnabled: true, - metricName: 'MetricForWebACLCDKRateLimit', - sampledRequestsEnabled: true, - }, - name: 'CustomIdentityWebACLRateLimit', - rules: [ - // Add rate limiting rule to allow 3.33 TPS from a single IP (1000 per 5 minutes) - { - name: 'RateLimitingRule', - priority: 1, - action: { - block: {} - }, - statement: { - rateBasedStatement: { - limit: 1000, - aggregateKeyType: 'IP' - } - }, - visibilityConfig: { - cloudWatchMetricsEnabled: true, - metricName: 'MetricForWebACLCDK-RateLimiting', - sampledRequestsEnabled: true, - } - }] - }); - // Define an API Gateway for the authentication component public endpoint const logGroup = new logs.LogGroup(this, "CustomIdentityAPiAccessLogs"); const api_gateway = new apigw.RestApi(this, 'ApiGateway', { @@ -217,27 +217,27 @@ export class CustomIdentityComponentStack extends Stack { deployOptions: { accessLogDestination: new apigw.LogGroupLogDestination(logGroup), accessLogFormat: apigw.AccessLogFormat.clf(), - loggingLevel: MethodLoggingLevel.ERROR, + loggingLevel : MethodLoggingLevel.ERROR, tracingEnabled: true, stageName: 'prod', } }); // cdk-nag suppression for the API Gateway default logs access NagSuppressions.addResourceSuppressions( - api_gateway, [{ - id: 'AwsSolutions-IAM4', - reason: "We are using the default CW Logs access of API Gateway", - },], true); + api_gateway,[{ + id: 'AwsSolutions-IAM4', + reason: "We are using the default CW Logs access of API Gateway", + },],true); // Attach the Web Application Firewall with the standard AWS provided rule set - new wafv2.CfnWebACLAssociation(this, 'ApiGatewayWebACLAssociation', { + new wafv2.CfnWebACLAssociation(this,'ApiGatewayWebACLAssociation',{ resourceArn: api_gateway.deploymentStage.stageArn, - webAclArn: cfnWebACLManaged.attrArn, + webAclArn:cfnWebACLManaged.attrArn, }); // Attach the WAF with the rate limit rules - new wafv2.CfnWebACLAssociation(this, 'ApiGatewayWebACLAssociationRateLimit', { + new wafv2.CfnWebACLAssociation(this,'ApiGatewayWebACLAssociationRateLimit',{ resourceArn: api_gateway.deploymentStage.stageArn, - webAclArn: cfnWebACLRateLimit.attrArn, + webAclArn:cfnWebACLRateLimit.attrArn, }); // Request validator for the API @@ -269,7 +269,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://" + distribution.domainName, + "ISSUER_URL": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -284,7 +284,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-as-guest').addMethod('GET', new apigw.LambdaIntegration(login_as_guest_function), { + api_gateway.root.addResource('login-as-guest').addMethod('GET', new apigw.LambdaIntegration(login_as_guest_function),{ requestParameters: { 'method.request.querystring.user_id': false, 'method.request.querystring.guest_secret': false, @@ -317,7 +317,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://" + distribution.domainName, + "ISSUER_URL": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, @@ -330,7 +330,7 @@ export class CustomIdentityComponentStack extends Stack { NagSuppressions.addResourceSuppressions(refresh_access_token_function_role, [ { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } ], true); - + // Map refresh_access_token_function to the api_gateway GET requeste refresh_access_token_function api_gateway.root.addResource('refresh-access-token').addMethod('GET', new apigw.LambdaIntegration(refresh_access_token_function), { requestParameters: { @@ -373,93 +373,93 @@ export class CustomIdentityComponentStack extends Stack { new CfnOutput(this, 'LoginEndpoint', { value: api_gateway.url }); // If Apple ID App ID Defined, add a DynamoDB table and Lambda function for Apple login - if (props.appleIdAppId != "") { - this.setupAppleIdLogin(props.appleIdAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if(props.appleIdAppId != "") { + this.setupAppleIdLogin(props.appleIdAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Steam App ID defined, add a DynamoDB table and Lambda function for Steam login - if (props.steamAppId != "") { - this.setupSteamLogin(props.steamAppId, props.steamWebApiKeySecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if(props.steamAppId != "") { + this.setupSteamLogin(props.steamAppId, props.steamWebApiKeySecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Google Play App ID defined, add a DynamoDB table and Lambda function for Google Play login - if (props.googlePlayClientId != "") { - this.setupGooglePlayLogin(props.googlePlayClientId, props.googlePlayAppId, props.googlePlayClientSecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if(props.googlePlayClientId != "") { + this.setupGooglePlayLogin(props.googlePlayClientId, props.googlePlayAppId, props.googlePlayClientSecretArn, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } // If Facebook App ID defined, add a DynamoDB table and Lambda function for Facebook login - if (props.facebookAppId != "") { - this.setupFacebookLogin(props.facebookAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); + if(props.facebookAppId != "") { + this.setupFacebookLogin(props.facebookAppId, secret, user_table, distribution, api_gateway, lambdaBasicPolicy, requestValidator); } } ///// *** IDENTITY PROVIDER SPECIFIC RESOURECE **** ////// // Sets up Lambda endpoint and DynamoDB table for Apple ID Login - setupAppleIdLogin(appId: string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - - // Define a DynamoDB table for AppleIdUsers - const appleIdUserTable = new dynamodb.Table(this, 'AppleIdUserTable', { - partitionKey: { - name: 'AppleId', - type: dynamodb.AttributeType.STRING - }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - pointInTimeRecovery: true - }); - - // Lambda function for Apple Id login - const loginWithAppleIdFunctionRole = new iam.Role(this, 'LoginWithAppleIdFunctionRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - }); - loginWithAppleIdFunctionRole.addToPolicy(lambdaBasicPolicy); - const loginWithAppleIdFunction = new lambda.Function(this, 'LoginWithAppleId', { - role: loginWithAppleIdFunctionRole, - code: lambda.Code.fromAsset("lambda", { - bundling: { - image: lambda.Runtime.PYTHON_3_11.bundlingImage, - command: [ - 'bash', '-c', - 'pip install --platform manylinux2014_x86_64 --only-binary=:all: -r requirements.txt -t /asset-output && cp -ru . /asset-output' - ], + setupAppleIdLogin(appId : string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + + // Define a DynamoDB table for AppleIdUsers + const appleIdUserTable = new dynamodb.Table(this, 'AppleIdUserTable', { + partitionKey: { + name: 'AppleId', + type: dynamodb.AttributeType.STRING + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + pointInTimeRecovery: true + }); + + // Lambda function for Apple Id login + const loginWithAppleIdFunctionRole = new iam.Role(this, 'LoginWithAppleIdFunctionRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + loginWithAppleIdFunctionRole.addToPolicy(lambdaBasicPolicy); + const loginWithAppleIdFunction = new lambda.Function(this, 'LoginWithAppleId', { + role: loginWithAppleIdFunctionRole, + code: lambda.Code.fromAsset("lambda", { + bundling: { + image: lambda.Runtime.PYTHON_3_11.bundlingImage, + command: [ + 'bash', '-c', + 'pip install --platform manylinux2014_x86_64 --only-binary=:all: -r requirements.txt -t /asset-output && cp -ru . /asset-output' + ], },}), - runtime: lambda.Runtime.PYTHON_3_11, - handler: 'login_with_apple_id.lambda_handler', - timeout: Duration.seconds(15), - tracing: lambda.Tracing.ACTIVE, - memorySize: 2048, - environment: { - "ISSUER_URL": "https://" + distribution.domainName, - "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", - "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", - "SECRET_KEY_ID": secret.secretName, - "USER_TABLE": user_table.tableName, - "APPLE_APP_ID": appId, - "APPLE_ID_USER_TABLE": appleIdUserTable.tableName - } - }); - secret.grantRead(loginWithAppleIdFunction); - user_table.grantReadWriteData(loginWithAppleIdFunction); - appleIdUserTable.grantReadWriteData(loginWithAppleIdFunction); - - NagSuppressions.addResourceSuppressions(loginWithAppleIdFunctionRole, [ - { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } - ], true); - - // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-apple-id').addMethod('GET', new apigw.LambdaIntegration(loginWithAppleIdFunction), { - requestParameters: { - 'method.request.querystring.apple_auth_token': true, - 'method.request.querystring.auth_token': false, - 'method.request.querystring.link_to_existing_user': false - }, - requestValidator: requestValidator - }); + runtime: lambda.Runtime.PYTHON_3_11, + handler: 'login_with_apple_id.lambda_handler', + timeout: Duration.seconds(15), + tracing: lambda.Tracing.ACTIVE, + memorySize: 2048, + environment: { + "ISSUER_URL": "https://"+distribution.domainName, + "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", + "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", + "SECRET_KEY_ID": secret.secretName, + "USER_TABLE": user_table.tableName, + "APPLE_APP_ID": appId, + "APPLE_ID_USER_TABLE": appleIdUserTable.tableName + } + }); + secret.grantRead(loginWithAppleIdFunction); + user_table.grantReadWriteData(loginWithAppleIdFunction); + appleIdUserTable.grantReadWriteData(loginWithAppleIdFunction); + + NagSuppressions.addResourceSuppressions(loginWithAppleIdFunctionRole, [ + { id: 'AwsSolutions-IAM5', reason: 'Using the standard Lambda execution role, all custom access resource restricted.' } + ], true); + + // Map login_as_guest_function to the api_gateway GET requeste login_as_guest + api_gateway.root.addResource('login-with-apple-id').addMethod('GET', new apigw.LambdaIntegration(loginWithAppleIdFunction),{ + requestParameters: { + 'method.request.querystring.apple_auth_token': true, + 'method.request.querystring.auth_token': false, + 'method.request.querystring.link_to_existing_user': false + }, + requestValidator: requestValidator + }); } // Sets up Lambda endpoint and DynamoDB table for Steam ID Login setupSteamLogin(appId: string, steamWebApiKeySecretArn: string, privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - + // Define a DynamoDB table for Steam Users const steamIdUserTable = new dynamodb.Table(this, 'SteamUserTable', { partitionKey: { @@ -491,7 +491,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://" + distribution.domainName, + "ISSUER_URL": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": privateKeySecret.secretName, @@ -517,7 +517,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-steam').addMethod('GET', new apigw.LambdaIntegration(loginWithSteamIdFunction), { + api_gateway.root.addResource('login-with-steam').addMethod('GET', new apigw.LambdaIntegration(loginWithSteamIdFunction),{ requestParameters: { 'method.request.querystring.steam_auth_token': true, 'method.request.querystring.auth_token': false, @@ -529,8 +529,8 @@ export class CustomIdentityComponentStack extends Stack { // Sets up Lambda endpoint and DynamoDB table for Google Play Login setupGooglePlayLogin(googlePlayClientId: string, googlePlayAppId: string, googlePlayClientSecretArn: string, - privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, - distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + privateKeySecret: secretsmanager.Secret, user_table: dynamodb.Table, + distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { // Define a DynamoDB table for Google Play const googlePlayUserTable = new dynamodb.Table(this, 'GooglePlayUserTable', { @@ -548,7 +548,7 @@ export class CustomIdentityComponentStack extends Stack { }); loginWithGooglePlayFunctionRole.addToPolicy(lambdaBasicPolicy); const loginWithGooglePlayFunction = new lambda.Function(this, 'LoginWithGooglePlay', { - role: loginWithGooglePlayFunctionRole, + role: loginWithGooglePlayFunctionRole, code: lambda.Code.fromAsset("lambda", { bundling: { image: lambda.Runtime.PYTHON_3_11.bundlingImage, @@ -563,7 +563,7 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://" + distribution.domainName, + "ISSUER_URL": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": privateKeySecret.secretName, @@ -590,7 +590,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-google-play').addMethod('GET', new apigw.LambdaIntegration(loginWithGooglePlayFunction), { + api_gateway.root.addResource('login-with-google-play').addMethod('GET', new apigw.LambdaIntegration(loginWithGooglePlayFunction),{ requestParameters: { 'method.request.querystring.google_play_auth_token': true, 'method.request.querystring.auth_token': false, @@ -600,9 +600,9 @@ export class CustomIdentityComponentStack extends Stack { }); } - // Sets up Lambda endpoint and DynamoDB table for Facebook Login - setupFacebookLogin(appId: string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { - + // Sets up Lambda endpoint and DynamoDB table for Facebook Login + setupFacebookLogin(appId : string, secret: secretsmanager.Secret, user_table: dynamodb.Table, distribution: cloudfront.Distribution, api_gateway: apigw.RestApi, lambdaBasicPolicy: iam.PolicyStatement, requestValidator: apigw.RequestValidator) { + // Define a DynamoDB table for Facebook Users const facebookUserTable = new dynamodb.Table(this, 'FacebookUserTable', { partitionKey: { @@ -634,12 +634,12 @@ export class CustomIdentityComponentStack extends Stack { tracing: lambda.Tracing.ACTIVE, memorySize: 2048, environment: { - "ISSUER_URL": "https://" + distribution.domainName, + "ISSUER_URL": "https://"+distribution.domainName, "POWERTOOLS_METRICS_NAMESPACE": "AWS for Games", "POWERTOOLS_SERVICE_NAME": "CustomIdentityComponent", "SECRET_KEY_ID": secret.secretName, "USER_TABLE": user_table.tableName, - "FACEBOOK_APP_ID": appId, + "FACEBOOK_APP_ID" : appId, "FACEBOOK_USER_TABLE": facebookUserTable.tableName } }); @@ -652,7 +652,7 @@ export class CustomIdentityComponentStack extends Stack { ], true); // Map login_as_guest_function to the api_gateway GET requeste login_as_guest - api_gateway.root.addResource('login-with-facebook').addMethod('GET', new apigw.LambdaIntegration(loginWithFacebookFunction), { + api_gateway.root.addResource('login-with-facebook').addMethod('GET', new apigw.LambdaIntegration(loginWithFacebookFunction),{ requestParameters: { 'method.request.querystring.facebook_access_token': true, 'method.request.querystring.facebook_user_id': true, @@ -661,5 +661,5 @@ export class CustomIdentityComponentStack extends Stack { }, requestValidator: requestValidator }); - } +} }; \ No newline at end of file From b92e4041797b6449604fc3f999e4b3fd28a98c0c Mon Sep 17 00:00:00 2001 From: Brian Morin Date: Sat, 30 Sep 2023 20:54:51 -0700 Subject: [PATCH 3/3] minor edits after self-review --- CustomIdentityComponent/lambda_rust/Cargo.toml | 2 +- CustomIdentityComponent/lambda_rust/src/main.rs | 2 +- CustomIdentityComponent/lib/custom_identity_component-stack.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CustomIdentityComponent/lambda_rust/Cargo.toml b/CustomIdentityComponent/lambda_rust/Cargo.toml index a4f4d17..203d5aa 100644 --- a/CustomIdentityComponent/lambda_rust/Cargo.toml +++ b/CustomIdentityComponent/lambda_rust/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "lambda-http-test" +name = "refresh-access-token" version = "0.1.0" edition = "2021" diff --git a/CustomIdentityComponent/lambda_rust/src/main.rs b/CustomIdentityComponent/lambda_rust/src/main.rs index 32657f4..a27ee6d 100644 --- a/CustomIdentityComponent/lambda_rust/src/main.rs +++ b/CustomIdentityComponent/lambda_rust/src/main.rs @@ -21,7 +21,7 @@ struct ClaimsIn { exp: i64, } -// Output Jwt token claims with references to save some allocations +/// Output Jwt token claims with references to save some allocations #[derive(Debug, Serialize)] struct ClaimsOut<'a> { sub: &'a str, diff --git a/CustomIdentityComponent/lib/custom_identity_component-stack.ts b/CustomIdentityComponent/lib/custom_identity_component-stack.ts index 4e91bbe..c465a27 100644 --- a/CustomIdentityComponent/lib/custom_identity_component-stack.ts +++ b/CustomIdentityComponent/lib/custom_identity_component-stack.ts @@ -346,7 +346,7 @@ export class CustomIdentityComponentStack extends Stack { bundling: { forcedDockerBundling: true, }, - timeout: Duration.seconds(15), + timeout: Duration.seconds(5), tracing: lambda.Tracing.ACTIVE, memorySize: 256, environment: {