6
votes

Using a .Net Core 1.0 Lambda I want to be able to create a Lambda function which handles the PreSignUp trigger from an AWS Cognito User pool.

using Amazon.Lambda.Core;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

public class PreSignUp_SignUp
{
  public string userPoolId { get; set; }
  public const string EmailKey = "email";
  public const string PhoneNumber = "phone_number";
  public Dictionary<string,string> userAttributes { get; set; }
  public Dictionary<string, string> validationData { get; set; }
}

public class PreSignup_SignUpResponse
{
  public bool autoConfirmUser { get; set; }
}

public class Function
{
  public PreSignup_SignUpResponse FunctionHandler(PreSignUp_SignUp input, ILambdaContext context)
  {
      return new PreSignup_SignUpResponse { autoConfirmUser = true };
  }
}

Though the request succeeds and returns a response when invoking the Lambda with an example request of:

{
  "datasetName": "datasetName",
  "eventType": "SyncTrigger",
  "region": "us-east-1",
  "identityId": "identityId",
  "datasetRecords": {
    "SampleKey2": {
      "newValue": "newValue2",
      "oldValue": "oldValue2",
      "op": "replace"
    },
    "SampleKey1": {
      "newValue": "newValue1",
      "oldValue": "oldValue1",
      "op": "replace"
    }
  },
  "identityPoolId": "identityPoolId",
  "version": 2
}

When performing an actual SignUp via the .Net AmazonCognitoIdentityProviderClient I get back an error:

Amazon.CognitoIdentityProvider.Model.InvalidLambdaResponseException : Unrecognizable lambda output

Which I'm guessing means I have not got the shape of the response (and possibly even request) correct.

Does anyone have an example of a .Net Lambda function that works for the PreSignUp trigger in AWS Cognito?

3

3 Answers

15
votes

The cognito trigger requests/responses must contain the entire payload as specified in the Cognito trigger documentation:

http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html

I have found while diagnosing this issue the best place to start is by creating a function handler that takes a JObject and then logs and return's that same object e.g.

public JObject FunctionHandler(JObject input, ILambdaContext context)
{
    context.Logger.LogLine("Input was: " + input);
    return input;
}

This captures the payload in cloudwatch logs and then helps steer you towards the strongly typed structured required.

In my case for PreSignUp I ended up creating the following types to make a simple function which auto-verifies all supplied credentials.

public abstract class AbstractTriggerRequest
{
    [JsonProperty("userAttributes")]
    public Dictionary<string, string> UserAttributes { get; set; }
}

public abstract class AbstractTriggerResponse
{
}

public class TriggerCallerContext
{
    [JsonProperty("awsSdkVersion")]
    public string AwsSdkVersion { get; set; }
    [JsonProperty("clientId")]
    public string ClientId { get; set; }
}

public abstract class AbstractTriggerBase<TRequest, TResponse>
    where TRequest: AbstractTriggerRequest
    where TResponse: AbstractTriggerResponse
{
    [JsonProperty("version")]
    public int Version { get; set; }
    [JsonProperty("triggerSource")]
    public string TriggerSource { get; set; }
    [JsonProperty("region")]
    public string Region { get; set; }
    [JsonProperty("userPoolId")]
    public string UserPoolId { get; set; }  
    [JsonProperty("callerContext")]
    public TriggerCallerContext CallerContext { get; set; }
    [JsonProperty("request")]
    public TRequest Request { get; set; }
    [JsonProperty("response")]
    public TResponse Response { get; set; }
    [JsonProperty("userName", NullValueHandling = NullValueHandling.Ignore)]
    public string UserName { get; set; }
}

public class PreSignUpSignUpRequest : AbstractTriggerRequest
{
    [JsonProperty("validationData")]
    public Dictionary<string,string> ValidationData { get; set; }
}

The Lambda function then ends up with the following signature:

public class Function
{
    public PreSignUp_SignUp FunctionHandler(PreSignUp_SignUp input, ILambdaContext context)
    {
        context.Logger.LogLine("Auto-confirming everything!");

        input.Response = new PreSignUpSignUpResponse {
            AutoConfirmUser = true,
            // you can only auto-verify email or phone if it's present in the user attributes
            AutoVerifyEmail = input.Request.UserAttributes.ContainsKey("email"),
            AutoVerifyPhone = input.Request.UserAttributes.ContainsKey("phone_number") 
        };

        return input;
    }
}

Hopefully this helps anyone else running into issues writing Lambda triggers for Cognito.

2
votes

There is already another great answer in here. However I'm not a expert .NET developer so this solution makes more sense to me.

class AutoVerifyEmail
{
    public AutoVerifyEmail() { }

    public JObject AutoVerifyEmailPreSignup(JObject input, ILambdaContext context)
    {
        //Console.Write(input); //Print Input

        input["response"]["autoVerifyEmail"] = true;
        input["response"]["autoConfirmUser"] = true;

        return input;
    }
}
2
votes

The previous two responses are now inaccurate unless you still use the old, less performant Amazon.Lambda.Serialization.Json.JsonSerializer. This old serializer uses Newtonsoft.Json while the new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer implements the recent System.Text.Json.

As a result, a JObject parameter is no longer appropriate and should instead be replaced with JsonElement. If you try to use JObject with this new serializer, you will get an error since the serializer doesn't know how to deal with this object.

You should read this to gain a better understanding of how it all works, but you access properties of the JsonElement using GetProperty("[insert property name here]").

For example:

public async Task<JsonElement> FunctionHandler(JsonElement input, ILambdaContext context)
{
    var request = input.GetProperty("request");
    var userAttributes = request.GetProperty("userAttributes");
    string email = userAttributes.GetProperty("email").GetString();

    return input;
}

This way, you don't need to construct entire classes to accommodate the required request and response parameters, just get and set the properties you need.