37

Here in the blue print says, API gateway will respond with 401: Unauthorized.

I wrote the same raise Exception('Unauthorized') in my lambda and was able to test it from Lambda Console. But in POSTMAN, I'm receiving status 500 with body:

{
  message: null`
} 

I want to add custom error messages such as "Invalid signature", "TokenExpired", etc., Any documentation or guidance would be appreciated.

Tenzin Chemi
  • 5,101
  • 2
  • 27
  • 33

7 Answers7

41

This is totally possible but the docs are so bad and confusing.

Here's how you do it:

There is an object called $context.authorizer that you have access to in your gateway responses template. You can read more about it here: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html

Here is an examample of populating this authorizer object from your authorizer lambda like so:

// A simple TOKEN authorizer example to demonstrate how to use an authorization token 
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke 
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke 
// the request if the token value is 'deny'. If the token value is 'Unauthorized', the function 
// returns the 'Unauthorized' error with an HTTP status code of 401. For any other token value, 
// the authorizer returns an 'Invalid token' error. 

exports.handler =  function(event, context, callback) {
    var token = event.authorizationToken;
    switch (token.toLowerCase()) {
        case 'allow':
            callback(null, generatePolicy('user', 'Allow', event.methodArn));
            break;
        case 'deny':
            
            callback(null, generatePolicy('user', 'Deny', event.methodArn));
            break;
        case 'unauthorized':
            callback("Unauthorized");   // Return a 401 Unauthorized response
            break;
        default:
            callback("Error: Invalid token"); 
    }
};

       var generatePolicy = function(principalId, effect, resource) {
            var authResponse = {};
            
            authResponse.principalId = principalId;
            if (effect && resource) {
                var policyDocument = {};
                policyDocument.Version = '2012-10-17'; 
                policyDocument.Statement = [];
                var statementOne = {};
                statementOne.Action = 'execute-api:Invoke'; 
                statementOne.Effect = effect;
                statementOne.Resource = resource;
                policyDocument.Statement[0] = statementOne;
                authResponse.policyDocument = policyDocument;
            }
            
            // Optional output with custom properties of the String, Number or Boolean type.
            authResponse.context = {
                "stringKey": "stringval custom anything can go here",
                "numberKey": 123,
                "booleanKey": true,
            };
            return authResponse;
        }

They key here is adding this part:

// Optional output with custom properties of the String, Number or Boolean type.

        authResponse.context = {
            "stringKey": "stringval custom anything can go here",
            "numberKey": 123,
            "booleanKey": true,
        };

This will become available on $context.authorizer

I then set the body mapping template in gateway responses tab like this:

{"message":"$context.authorizer.stringKey"}

NOTE: it must be quoted!

finally - after sending a request in postman with Authorization token set to deny I now get back a payload from postman that looks like this:

{
    "message": "stringval custom anything can go here"
}
maxwell
  • 2,313
  • 23
  • 35
  • 5
    You should keep in mind that Gateway Responses are only applied after you re-deploy your API. You can also utilize `Response Headers` instead of editing the default `Body Mapping Template` to keep global 403 errors to return correct error message body. – hoonoh Jul 01 '18 at 12:18
  • Even if that works, looks pretty hacky. According to AWS docs, the purpose of that context object is: "In addition to returning an IAM policy, the Lambda authorizer function must also return the caller's principal identifier. It can also optionally return a context object containing additional information that can be passed into the integration backend. For more information, see Output from an Amazon API Gateway Lambda Authorizer." Source: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html – Mauri Q Oct 22 '19 at 18:55
  • 3
    If I throw an error from inside the authorizer like `throw new Error("blabla")`, why the error message is not `{message: "blabla"}`? Without such behavior we can't send custom error messages with 500 status codes. – walter_dl Jan 08 '20 at 02:11
  • @walter_dl have you find any solution? I tried as mentioned by maxwell but it's not working for me – Vishal Patel Apr 22 '20 at 20:10
  • What's the advantage of returning a 401: "Unauthorized" (or any other denial message), versus generating a DENY IAM policy? – EvilJordan Sep 20 '20 at 06:32
  • @Maxwell Thank you so much. It was of great help. – Naik Ashwini Sep 22 '20 at 10:06
  • 3
    NOTE: This only seems to work for the 403 "Access Denied" gateway response. I still do not see a way to pass context to the 401 "Unauthorized" gateway response. That can only be triggered by `callback("Unauthorized")` which does not receive the authorizer context. This seems like a huge limitation by AWS. – Connor Mar 29 '21 at 21:55
  • 1
    @walter_dl, whichever language you choose, it would be best to just have a catch-all exception and return a 403 access denied alongside a deny IAM permission set. This means you can set `$context.authorizer.message`, and change the **Access Denied** Gateway Response, and be able to set custom error messages. – mohoromitch Oct 26 '21 at 20:25
6

I used @maxwell solution, using custom resource ResponseTemplates. For deny response see below:

{
  "success":false,
  "message":"Custom Deny Message"
}

You can check this out here: https://github.com/SeptiyanAndika/serverless-custom-authorizer

maxwell
  • 2,313
  • 23
  • 35
3

Maxwell is mostly correct. I tried his implementation and noticed that his message should go from :

{"message":"$context.authorizer.stringKey"}

to

{"message":"$context.authorizer.context.stringKey"}

3

As noted by Connor far as I can see, the answer to the specific question - which mentions 401 related errors - is NO.

You can produce a generic 401 Unauthorized but you cannot alter the error message.

That is you can customise the 403 Forbidden (DENY) messages but not the 401's.

Note that I've used the NodeJS Lambda custom authorizers but not the Python version referenced in the question.

Daniel
  • 717
  • 6
  • 21
2

I'm not sure what is causing the 500 message: null response. Possibly misconfiguration of the Lambda function permissions.

To customize the Unauthorized error response, you'll set up a Gateway Response for the UNAUTHORIZED error type. You can configure response headers and payload here.

jackko
  • 6,998
  • 26
  • 38
  • 2
    Since the original poster is raising a raw `Exception`, the lambda authorization function errors out, which means it returns a 500 error. In API Gateway, this triggers the `Authorizer Failure` Gateway response, which has a default template of `{"message":$context.error.messageString}`. The default value of this key since the lambda "crashed" is empty, hence the return being {"message":null}. The `$context.error.messageString`property was never set. – mohoromitch Oct 26 '21 at 20:20
1

With my testing what i observed is , You cannot customize message when you throw exception from the lambda, You can have customized messages when you return DENY Policy message from the authorizer

Here is how i am returning custom message when i DENY from the Authorizer, it in the detail field of authResponse.context returned from custom Authorizer

you can also update status code to 401 instead of 403 .

enter image description here

ravthiru
  • 8,878
  • 2
  • 43
  • 52
-3

This can be easily achieved by using the context.fail() function.

Example:

const customAuthorizer: Handler = (event, context: Context, callback: Callback) => {        
    authenticate(event)
        .then((res) => {
            // result should be as described in AWS docs
            callback(null, res);
        })
        .catch((err) => {
            context.fail("Unauthorized");
        });
}

This will return a 401 response with following body.

{
    "message": "Unauthorized"
}

This can also be achieved by throwing an error:

throw new Error('Unauthorized');
Lasitha Yapa
  • 4,309
  • 8
  • 38
  • 57
  • 1
    This will allow you to deny a request, but there is no mention of a custom error response message, as request in the original question – Dre Oct 24 '19 at 01:06