sloev/python-lambdarest

[Feature Request] ALB Custom Scopes

andyfase opened this issue · 2 comments

issue / feature request

I have a need to perform fine-grained permission checking on a Lambda function behind a ALB. I have extended lambdarest to allow for the existing Scopes functionality to also verify scopes passed from an ALB as it currently does from API Gateway. I would like to provide this code into the core code base - hence wanting to ensure you would be happy to accept it before I raise a PR.

Unlike custom authorizers in API GW, ALB's do not provide a "scope" attribute or claim that is standardized in the event object - I have allowed the developer to specify the "scope" claim that is used (in the call to create_lambda_handler) and then have added code to extract and verify the scopes from the x-amzn-oidc-data header when the request is parsed.

Parsing the ALB header requires the use of several external libraries (base64, jwt, requests) as the header data needs to be decoded, the signature checked (hence the ALB public key fetched). Hence this is not just an addition of a few lines of Python but requires the base dependency to be extended.

I think this could be useful to the wider users of this library - but wanted ton confirm before raising a PR

Thanks!

Example Helper Class to verify ALB Scopes

class ALBScopes:
  def __init__(self, scope_param):
    self.scope_param = scope_param
    self.alb_public_keys={}

  def __get_public_key(self, kid):
    url = 'https://public-keys.auth.elb.' + \
      os.environ['AWS_REGION'] + '.amazonaws.com/' + kid
    req = requests.get(url)
    return req.text

  def __verify_oidc_data(self, data):
    jwt_headers = data.split('.')[0]
    decoded_jwt_headers = base64.b64decode(jwt_headers)
    decoded_json = json.loads(decoded_jwt_headers)
    key_id = decoded_json['kid']
    if decoded_json['kid'] not in self.alb_public_keys:
      self.alb_public_keys[key_id] = self.__get_public_key(key_id)
    return jwt.decode(data, self.alb_public_keys[key_id], algorithms=['ES256'])

  def get_scopes(self, event):
    encoded_data = event.get('headers', {}).get('x-amzn-oidc-data', None)
    data = self.__verify_oidc_data(encoded_data)
    if isinstance(data.get(self.scope_param, None), list):
      return data.get(self.scope_param, None)
    return []
sloev commented

@andyfase i think it makes sense but i dont completely comprehend it yet. Would you be up for a google meet call or something as i think it would be quicker to pass an understanding in that medium.

a second thing:
increasing lines of code by using stdlib (urllib etc) could maybe reduce the list of dependencies (requests can be dropped atleast) decreasing the build size

sloev commented

leaving this issue open for the future, but it needs more info @andyfase