16

I am using a new feature of the API Gateway with Lambda functions to use a Custom Authorizer (https://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html).

The authorizer uses JWT Tokens to validate the token for the current user context and scope(s). All is working fine but there is one concept regarding AWS Policies that I cannot quite figure out from documentation.

The output of a Custom Authorizer function has to be an object containing two things:

  1. principalId - in question
  2. policyDocument - a valid Policy document with statements to allow user authorized access to Lambda Resources, stages, etc.

Now, the examples for Custom Authorizers currently show almost arbitrary values for the principalId variable. But if I am thinking correctly, this principalId should be unique for each user? And perhaps have a user specific unique value associated with it (such as token.userId or token.email).

If this is true, then for my provided code below, if the JWT Token is not valid, then I do not have access to the userId or email, and have no clue what to set the principalId to. I am setting it temporarily to user just to have something return for a Deny policy to ensure that the response is 403 Forbidden.

Anyone have any clue on best practices for setting principalId for a Custom Authorizer?

var jwt = require('jsonwebtoken');
var JWT_SECRET = 'My$ecret!';


/**
 * Implicit AWS API Gateway Custom Authorizer. Validates the JWT token passed
 * into the Authorization header for all requests.
 * @param  {Object} event   [description]
 * @param  {Object} context [description]
 * @return {Object}         [description]
 */
exports.handler = function(event, context) {
  var token = event.authorizationToken;
  try {
    var decoded = jwt.verify(token, JWT_SECRET);
    context.done(null, generatePolicy(decoded.id, 'Allow', 'arn:aws:execute-api:*:*:*'));
  } catch(ex) {
    console.error(ex.name + ": " + ex.message);
    context.done(null, generatePolicy('user', 'Deny', 'arn:aws:execute-api:*:*:*'));
  }
};

function generatePolicy(principalId, effect, 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;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyDocument;
  }
  return authResponse;
}
Jedi
  • 3,088
  • 2
  • 28
  • 47
Tom Pennetta
  • 482
  • 7
  • 25

1 Answers1

15

The principalId is intended to represent the long term identifier for whatever entity is being authorized to make the API call. So if you have an existing database of users, each user presumably has a unique identifier or username. You mentioned 'user', which is probably fine. Functionally, the principalId is logged if you enable CloudWatch Logs, and is also what you can access in the $context for mapping templates.

In terms of design for your function, you have two options for dealing with an 'invalid' token.

  1. If you return a valid policy that denies access, this helps you by caching the policy associated with the token in case it's used again, so you get fewer Lambda invocations. However the client may receive a 403 and think that the token was valid but they don't have access to the resource they requested.

  2. context.fail("Unauthorized") will send a 401 response bad to the client, which should indicate to them that the token was invalid. This would help the client, but also result in more invocations on the function if the client repeatedly replayed the bad token. Negative caching is currently not available on the feature, but another way to provide moderate protection is to use the 'identityValidationExpresion' -> http://docs.aws.amazon.com/apigateway/api-reference/resource/authorizer/#identityValidationExpression

Also, I'd strongly recommend that you migrate this to a new Lambda function based on the apigateway-authorizer-nodejs blueprint, since the code sample in the docs is minimal and intended for illustration only. The blueprint has lots of comments that document various uses, like the fail("Unauthorized") functionality.

jackko
  • 6,998
  • 26
  • 38
  • Thanks for this! I do still have a clarification question regarding just the principalId of a returned generated policy. Let me run through a scenario: 1.) User logged in and has a valid JWT token that they can now pass in the Authorization header of each request. 2.) The custom authorizer can validate the token and ensure authorization based on the payload of the decoded JWT. 3.) The principal ID can now be something like: user|1234-abcd-4312-bcdef This enables me to have a unique identifier for all requests for this particular user. – Tom Pennetta Feb 19 '16 at 18:27
  • 1.) User has not logged in and requests a secured endpoint on the API Gateway. 2.) Custom Authorizer attempts to verify and decode the JWT but it is invalid/null. 3.) The principalId now has no unique identifier for this because of course this is not a valid user attempting access. What is the best practice for principalId value here? Perhaps user|Unauthorized ? I guess my struggle is also, should the principalId be unique for each user? Or for each User permission group? And what about for a failed or unauthorized +Custom Authorizer* request? – Tom Pennetta Feb 19 '16 at 18:27
  • Hi Tom, If the client doesn't send the header at all, or they send it with an empty/null value, API Gateway will throw this request back with a 401 immediately. In the case of an invalid token, you have two options: 1. If you want to have the result cached for this invalid token, "user|Unauthorized" is fine and you can send a DenyAll policy. 2. If you don't care about caching and you want the client to know that their token was invalid, use the context.fail("Unauthorized") line, indicating that the client should attempt to login or get a valid token in some way before trying again. – jackko Feb 22 '16 at 17:32
  • 1
    Again, the functional use of the principalId is up to you; it is available in the $context for mapping templates if you want to pass it along to your backend. It is also logged if you enable CloudWatch Logs – jackko Feb 22 '16 at 17:33
  • @JackKohn-AWS Thanks for your great answer here - covers exactly what I was struggling with. I haven't been able to verify this with certainty in the documentation... is the statement "negative caching is currently not available" for the fail("Unauthorized") functionality still currently valid? – kander Mar 20 '19 at 10:09