Why is my API Gateway proxy resource with a Lambda authorizer that has caching activated returning HTTP 403 "User is not authorized to access this resource" errors?

5 minute read
2

My Amazon API Gateway proxy resource with an AWS Lambda authorizer that has caching activated returns the following HTTP 403 error message: "User is not authorized to access this resource". Why is this happening, and how do I resolve the error?

Short description

Note: API Gateway can return 403 User is not authorized to access this resource errors for a variety of reasons. This article addresses 403 errors related to API Gateway proxy resources with a Lambda authorizer that has caching activated only. For information on troubleshooting other types of 403 errors, see How do I troubleshoot HTTP 403 errors from API Gateway?

A Lambda authorizer's output returns an AWS Identity and Access Management (IAM) policy to API Gateway. The IAM policy includes an explicit API Gateway API "Resource" element that's in the following format:

"arn:aws:execute-api:<region>:<account>:<API_id>/<stage>/<http-method>/[<resource-path-name>/[<child-resources-path>]"

When Authorization Caching is activated on a Lambda authorizer, the returned IAM policy is cached. The cached IAM policy is then applied to any additional API requests made within the cache's specified time-to-live (TTL) period.

If the API has a proxy resource with a greedy path variable of {proxy+}, the first authorization succeeds. Any additional API requests made to a different path within the cache's TTL period fail and return the following error:

"message": "User is not authorized to access this resource"

The additional requests fail, because the paths don't match the explicit API Gateway API "Resource" element defined in the cached IAM policy.

To resolve the issue, you can modify the Lambda authorizer function's code to return a wildcard (*/*) resource in the output instead. For more information, see Resources and conditions for Lambda actions.

Note: To activate authorizer caching, your authorizer must return a policy that is applicable to all methods across an API Gateway. The Lambda authorizer function's code must return a wildcard (*/*) resource in the output to allow all resources. The cache policy expects the same resource path cached, unless you made the same request twice on the same resource-path.

Resolution

Note: Modify the example Lambda authorizer function code snippets in this article to fit your use case.

In the following example setups, the Lambda functions extract the API Gateway's id value from the method's Amazon Resource Name (ARN) ( "event.methodArn"). Then, the functions define a wildcard "Resource" variable by combining the method ARN's paths with the API's id value and a wildcard ( */*).

Example token-based Lambda authorizer function code that returns a wildcard "Resource" variable

exports.handler =  function(event, context, callback) {
    var token = event.authorizationToken;
    var tmp = event.methodArn.split(':');
    var apiGatewayArnTmp = tmp[5].split('/');
    
    // Create wildcard resource
    var resource = tmp[0] + ":" + tmp[1] + ":" + tmp[2] + ":" + tmp[3] + ":" + tmp[4] + ":" + apiGatewayArnTmp[0] + '/*/*'; 
    switch (token) {
        case 'allow':
            callback(null, generatePolicy('user', 'Allow', resource));
            break;
        case 'deny':
            callback(null, generatePolicy('user', 'Deny', resource));
            break;
        case 'unauthorized':
            callback("Unauthorized");   // Return a 401 Unauthorized response
            break;
        default:
            callback("Error: Invalid token"); // Return a 500 Invalid token response
    }
};
// Help function to generate an IAM policy
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",
        "numberKey": 123,
        "booleanKey": true
    };
    return authResponse;
}

Example request parameter-based Lambda authorizer function code that returns a wildcard "Resource" variable

exports.handler = function(event, context, callback) {        
   
    // Retrieve request parameters from the Lambda function input:
    var headers = event.headers;
    var queryStringParameters = event.queryStringParameters;
    var pathParameters = event.pathParameters;
    var stageVariables = event.stageVariables;
        
    // Parse the input for the parameter values
    var tmp = event.methodArn.split(':');
    var apiGatewayArnTmp = tmp[5].split('/');

    // Create wildcard resource
    var resource = tmp[0] + ":" + tmp[1] + ":" + tmp[2] + ":" + tmp[3] + ":" + tmp[4] + ":" + apiGatewayArnTmp[0] + '/*/*'; 
    console.log("resource: " + resource);
    // if (apiGatewayArnTmp[3]) {
    //     resource += apiGatewayArnTmp[3];
    // }
        
    // Perform authorization to return the Allow policy for correct parameters and 
    // the 'Unauthorized' error, otherwise.
    var authResponse = {};
    var condition = {};
    condition.IpAddress = {};
     
    
    if (headers.headerauth1 === "headerValue1"
        && queryStringParameters.QueryString1 === "queryValue1"
        && stageVariables.StageVar1 === "stageValue1") {
        callback(null, generateAllow('me', resource));
    }  else {
        callback("Unauthorized");
    }
}
     
// Help function to generate an IAM policy
var generatePolicy = function(principalId, effect, resource) {
    // Required output:
    console.log("Resource in generatePolicy(): " + resource);
    var authResponse = {};
    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; // default version
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; // default action
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        console.log("***Resource*** " + resource);
        policyDocument.Statement[0] = statementOne;
        console.log("***Generated Policy*** ");
        console.log(policyDocument);
        authResponse.policyDocument = policyDocument;
    }
    // Optional output with custom properties of the String, Number or Boolean type.
    authResponse.context = {
        "stringKey": "stringval",
        "numberKey": 123,
        "booleanKey": true
    };
    
    return authResponse;
}
     
var generateAllow = function(principalId, resource) {
    return generatePolicy(principalId, 'Allow', resource);
}
     
var generateDeny = function(principalId, resource) {
    return generatePolicy(principalId, 'Deny', resource);
}

For more information on how to edit Lambda function code, see Deploying Lambda functions defined as .zip file archives.


Related information

Edit code using the console editor

AWS OFFICIAL
AWS OFFICIALUpdated 2 years ago
2 Comments

Instead of returning a 403 when the paths don't match, why not just call the Lambda authorizer again, since it should be up to the authorizer method to determine whether it truly is a 403.

harry
replied 5 months ago

Thank you for your comment. We'll review and update the Knowledge Center article as needed.

profile pictureAWS
MODERATOR
replied 5 months ago