Authorizer with API Gateway & Lambda

Custom Authorizer Architecture

OAuth, SAML 등 다양한 authentication 방법들을 API Gateway의 custom authorization을 통해서 컨트롤 할 수 있습니다. Client가 request를 API Gateway로 authorization token을 헤더로 포함해서 보내면, 해당 Request를 Lambda로 보내고, Lambda는 다시 IAM Policies를 리턴시켜서 보냅니다. Policy가 유효하지 않거나, Denied가 되면, 해당 API에대한 call은 실패하게 됩니다. Valid Policy를 보낸다면, API Gateway는 returned policy를 캐쉬시키고, 동일한 토큰을 갖고서 요청하는 모든 requests를 미리 설정된 TTL(기본값 3600초)값 동안 Lambda호출없이 처리하게 됩니다. (현재 Maximum TTL은 3600초이며, 그 이상 넘어갈수 없으며, 0초로 만들어서 캐쉬를 없앨수도 있습니다.)

Create Custom Authorizer Lambda Function

새로 만드는 Lambda Function이 AWS의 다른 서비스를 호출한다면, 먼저 execution role 설정을 통해서 권한 부여가 필요합니다.

API Gateway -> Lambda

{
    "type":"TOKEN",
    "authorizationToken":"<caller-supplied-token>",
    "methodArn":"arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>"
}
Name Description
authorizationToken 클라이언트가 Api Gateway로 request의 header에 붙여서 보내는 Auth-Token의 개념
type payload type을 정의하며, 현재의 유일한 유효한 값은 “TOKEN” literal 하나입니다.
methodArn API Gateway가 Lambda function에 값을 보내기전에 자동으로 넣어서 보냅니다.

Authorizer function in Lambda -> API Gateway

Customer authorizer’s Lambda function은 반드시 principal identifier 그리고 policy document를 포함한 response를 리턴시켜야 합니다.

{
  "principalId": "xxxxxxxx",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow|Deny",
        "Resource": "arn:aws:execute-api:<regionId>:<accountId>:<appId>/<stage>/<httpVerb>/[<resource>/<httpVerb>/[...]]"
      }
    ]
  }
}

// Example
// GET Method를 Deny 시키는 예제 입니다.
{
  "principalId": "user",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Deny",
        "Resource": "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/"
      }
    ]
  }
}
Name Description
Effect Allow, Deny로 해당 API Gateway의 Action을 실행시킬지 말지 결정합니다.
Action Action은 Resource를 정의하는 API Gateway Execution Service입니다.
Resource * (wild card)를 사용해서 Resource 를 정의할수 있습니다.
PrincipalId $context.authorizer.principalId 변수를 사용해 mapping table 에 access할 수 있습니다.

Create a Custom Authorization for API Methods

Mapping Templates

API Gateway는 Request, Response 데이터를 Backend 그리고 Client에 맞게끔 변환시켜줄 수 있으며, 또한 Validation의 기능이 있습니다.

Models

{
  "department": "produce",
  "categories": [
    "fruit",
    "vegetables"
  ],
  "bins": [
    {
      "category": "fruit",
      "type": "apples",
      "price": 1.99,
      "unit": "pound",
      "quantity": 232
    },
    {
      "category": "fruit",
      "type": "bananas",
      "price": 0.19,
      "unit": "each",
      "quantity": 112
    },
    {
      "category": "vegetables",
      "type": "carrots",
      "price": 1.29,
      "unit": "bag",
      "quantity": 57
    }
  ]
}

위의 JSON Data는 다음과 같은 Model로 정의될수 있습니다.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "GroceryStoreInputModel",
  "type": "object",
  "properties": {
    "department": { "type": "string" },
    "categories": {
      "type": "array",
      "items": { "type": "string" }
    },
    "bins": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "category": { "type": "string" },
          "type": { "type": "string" },
          "price": { "type": "number" },
          "unit": { "type": "string" },
          "quantity": { "type": "integer" }
        }
      }
    }
  }
}
Name Description
$schema JSON Schema version 을 나타냅니다.
title 사람이 읽을수 있는 Identifier 입니다.
type object, array, string, number, integer 등이 들어갈수 있습니다.
properties type이 object이면 안에 들어가는 내용물들

또한 추가적으로 minimum, maximum, string lengths, numeric values, array item lengths, regular expressions 등등을 더 추가해줄수 있습니다.

Mapping Templates

Mapping Templates은 data 를 다른 형식으로 변환하는데 사용이 됩니다. Mapping Templates을 정의하기 위해서 API Gateway는 Velocity Template Language 또는 JsonPath Expressions을 사용합니다. Input mapping Templates 그리고 Output mapping templates을 각각 따로 만들어줘야 합니다. 아래의 예제는 JSON데이터를 받아서 JSON으로 만들어주는 ㅡㅡ;; 예제입니다.

#set($inputRoot = $input.path('$'))
{
  "department": "$inputRoot.department",
  "categories": [
#foreach($elem in $inputRoot.categories)
    "$elem"#if($foreach.hasNext),#end
        
#end
  ],
  "bins" : [
#foreach($elem in $inputRoot.bins)
    {
      "category" : "$elem.category",
      "type" : "$elem.type",
      "price" : $elem.price,
      "unit" : "$elem.unit",
      "quantity" : $elem.quantity
    }#if($foreach.hasNext),#end
        
#end
  ]
}

Authentication Example with DynamoDB

Login

API Gateway에서는 Post로 하고, Authorization을 NONE으로 해줍니다. 즉 Login에서는 DynamoDB에 auth_token에 해당하는 principal_id만 새로 업데이트 해주고, 클라이언트에 새롭게 생성된 principal_id를 넘겨주면 됩니다.

from __future__ import print_function

import boto3
import json
import random
from hashlib import sha512
from string import digits, ascii_letters

# Constants
USER_TABLE_NAME = 'ss_user'

def lambda_handler(event, context):
    user_id = event.get('user_id')
    password = event.get('password')
    
    if not user_id or not password:
        raise Exception('unauthorized')
    
    # Clean Password
    _sha = sha512()
    _sha.update(password)
    password_digested = _sha.hexdigest()
    
    #print("Received event: " + json.dumps(event, indent=2))
    
    # Init DynamoDB
    dynamo = boto3.resource('dynamodb').Table(USER_TABLE_NAME)
    
    # Retrieve the user from DynamoDB
    user_obj = dynamo.get_item(Key={'user_id': user_id})
    
    # Authenticate the user
    if not user_obj:
        raise Exception('unauthorized')
        
    if not user_obj['Item']:
        raise Exception('unauthorized')
    
    if user_obj['Item']['password'] != password_digested:
        raise Exception('unauthorized')
    
    
    # Login the user
    principal_id = random_principal_id(10)
    
    # Update Principal ID on DynamoDB
    dynamo.update_item(Key={'user_id': user_id}, 
        UpdateExpression="set principal_id = :p",
        ExpressionAttributeValues={':p': principal_id})
    
    return {'auth_token': u'%s:%s' % (user_id, principal_id)}
    
    # operations = {
    #     'create': lambda x: dynamo.put_item(**x),
    #     'read': lambda x: dynamo.get_item(**x),
    #     'update': lambda x: dynamo.update_item(**x),
    #     'delete': lambda x: dynamo.delete_item(**x),
    #     'list': lambda x: dynamo.scan(**x),
    #     'echo': lambda x: x,
    #     'ping': lambda x: 'pong'
    # }
        
def random_principal_id(n):
    chars =  digits + ascii_letters
    return ''.join(random.choice(chars) for _ in range(n))

Custom Authorizer