using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using System.Net;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Amazon.DynamoDBv2;
using Newtonsoft.Json;
using Amazon.DynamoDBv2.DataModel;
using System.Threading.Tasks;
using System;
using System.Net.Http;
using System.Linq;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace Lambda.AuthFunction
{
public class Function
{
private string _jwks { get; set; }
private string _clientId { get; set; }
private string _userPool { get; set; }
private readonly AmazonDynamoDBClient _dynamoDbClient;
private readonly DynamoDBContext _context;
///
/// Default constructor to read environment variables, Get the JWKs, and initialize DynamoDB context
///
///
///
///
public Function()
{
LambdaLogger.Log("Initiating the default values");
string? envRegion = Environment.GetEnvironmentVariable("REGION");
string? envCognitoUserPoolId = Environment.GetEnvironmentVariable("COGNITO_USER_POOL_ID");
string? envClientId = Environment.GetEnvironmentVariable("CLIENT_ID");
if (String.IsNullOrEmpty(envRegion) || String.IsNullOrEmpty(envCognitoUserPoolId) || String.IsNullOrEmpty(envClientId))
throw new ArgumentNullException("REGION or COGNITO_USER_POOL_ID or CLIENT_ID");
_clientId = envClientId;
_userPool = String.Format("https://cognito-idp.{0}.amazonaws.com/{1}", envRegion, envCognitoUserPoolId);
string keyUrl = _userPool + "/.well-known/jwks.json";
_jwks = GetJWKs(keyUrl).Result;
_dynamoDbClient = new AmazonDynamoDBClient();
_context = new DynamoDBContext(_dynamoDbClient);
}
///
/// Method to make GET JWKs by calling Cognito User pool Key URL
///
///
/// Task
private async Task GetJWKs(string keyUrl)
{
HttpClient client = new HttpClient();
return await client.GetStringAsync(keyUrl).ConfigureAwait(continueOnCapturedContext: false);
}
///
/// Lambda function handler to validate JWT token
///
///
///
/// APIGatewayCustomAuthorizerResponse
public APIGatewayCustomAuthorizerResponse FunctionHandler(APIGatewayCustomAuthorizerRequest request, ILambdaContext context)
{
LambdaLogger.Log("Received Auth request");
/* Validating JWT token in three steps as documented at - https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
Step 1: Confirm the structure of the JWT
Step 2: Validate the JWT signature
Step 3: Verify the claims
*/
string token = GetTokenFromRequest(request.AuthorizationToken);
//Step 1: Confirm the structure of the JWT
if (!IsValidJwtStructure(token))
throw new Exception("Unauthorized");
//Step 2: Validate the JWT signature
JwtSecurityToken? jwtSecurityToken = ValidateJwtSignature(token);
if (jwtSecurityToken == null)
throw new Exception("Unauthorized");
//Step 3: Verify the claims
string userGroup = VerifyClaims(jwtSecurityToken);
if (String.IsNullOrEmpty(userGroup))
throw new Exception("Unauthorized");
// Get policy document based on user group
string policyDocument = GetApiGwAccessPolicy(userGroup);
if (String.IsNullOrEmpty(policyDocument))
{
//Return deny policy
return new APIGatewayCustomAuthorizerResponse
{
PrincipalID = "yyyyyyyy",
PolicyDocument = JsonConvert.DeserializeObject(GetDenyPolicy()),
Context = { },
UsageIdentifierKey = ""
};
}
return new APIGatewayCustomAuthorizerResponse
{
//Return access policy
PrincipalID = "yyyyyyyy",
PolicyDocument = JsonConvert.DeserializeObject(policyDocument),
Context = { },
UsageIdentifierKey = ""
};
}
///
/// Get token from request
///
///
/// string
private string GetTokenFromRequest(string authorizationHeader)
{
string authToken = String.Empty;
if (!String.IsNullOrEmpty(authorizationHeader))
{
var authHeaders = authorizationHeader.Split(" ");
LambdaLogger.Log("authHearers.Count(): " + authHeaders.Count());
if (authHeaders.Count() == 2 && authHeaders[0] == "Bearer")
{
return authHeaders[1];
}
}
return authToken;
}
///
/// Validate JWT structure
///
///
/// bool
private bool IsValidJwtStructure(string token)
{
if (String.IsNullOrEmpty(token))
return false;
if (token.Split(".").Count() != 3)
return false;
return true;
}
///
/// Verify JWT claims and return user group
///
///
/// string
private string VerifyClaims(JwtSecurityToken? jwtSecurityToken)
{
if (jwtSecurityToken == null)
return String.Empty;
//Note: Token expiration already verified in ValidateJwtSignature method.
try
{
var clientId = jwtSecurityToken.Claims.First(x => x.Type == "client_id").Value;
if (clientId != _clientId)
return String.Empty;
var iss = jwtSecurityToken.Claims.First(x => x.Type == "iss").Value;
if (iss != _userPool)
return String.Empty;
var tokenUse = jwtSecurityToken.Claims.First(x => x.Type == "token_use").Value;
if (tokenUse != "access")
return String.Empty;
return jwtSecurityToken.Claims.First(x => x.Type == "cognito:groups").Value;
}
catch (Exception)
{
//Exception when claim is missing
return String.Empty;
}
}
///
/// Validate JWT signature
///
///
/// JwtSecurityToken
private JwtSecurityToken? ValidateJwtSignature(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var signingKeys = new JsonWebKeySet(_jwks).GetSigningKeys();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
IssuerSigningKeys = signingKeys,
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero //set expiration time same as JWT expiration time
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
return jwtToken;
}
catch (Exception)
{
// return null if JWT validation fails
return null;
}
}
///
/// Get API GW access policy document
///
///
/// string
private string GetApiGwAccessPolicy(string userGroup)
{
var data = _context.LoadAsync(userGroup).Result;
if (data != null)
{
return data.ApiGwAccessPolicy;
}
return String.Empty;
}
///
/// Get API GW deny policy document
///
/// string
private string GetDenyPolicy()
{
string denyPolicy = "{\"Version\": \"2012-10-17\",\"Statement\": [ {\"Effect\": \"Deny\",\"Principal\": \"*\", \"Action\": [\"execute-api:Invoke\"], \"Resource\": [ \"arn:aws:execute-api:*:*:*\"]}]}";
return denyPolicy;
}
}
}