亚马逊AWS官方博客

通过Keycloak结合OAuth2.0协议进行AWS API Gateway鉴权

1. 简介

本文介绍了如何通过Keycloak,并结合AWS API Gateway内置的授权功能,完成对AWS资源请求的鉴权过程。API Gateway帮助开发者安全的的创建、发布、维护并管理API的访问。在中国区,由于Cognito仍未上线,因此使用Keycloak作为API调用的鉴权服务,具有重要的实际意义。

本文共分为四大模块:

简介:对Keycloak、OAuth2.0以及AWS API Gateway Authorizer进行了介绍;

配置说明:对环境的设定进行了详细的说明,包括DynamoDB的设定以及Keycloak环境的搭建和设置;

验证JWT Authorizer:通过Postman对API Gateway的授权功能进行验证;

总结:对全文的总结。

1.1 关于Keycloak

Keycloak是一个开源并广泛应用于用户身份管理与授权的解决方案。Keycloak支持多种协议和标准,包括OpenID Connect,OAuth2.0和SAML2.0。同时Keycloak可以集成与已有的LDAP或者Active Directory 服务集成,用于单点登录。基于OAuth2.0,Keycloak还可以通过JWT Token完成对API的鉴权,本文基于此场景,结合Keycloak通过AWS Gateway的Authorizer完成请求的鉴权。

1.2 关于OAuth2.0

OAuth2.0全称为Open Authorization 2.0,为用于鉴权的协议。通过OAuth协议,可以授权第三方应用请求用户的资源,而不需要资源的拥有者直接向第三方提供任何验证凭据信息。
OAuth2.0鉴权流程

1.3 关于AWS API Gateway Authorizer

在本文中,我们以HTTP API为例,利用HTTP API已内置的授权功能进行API请求的鉴权。如果使用REST API,则需要通过结合Lambda进行JWT Token的校验,可参照此说明文档进行配置。

2. 配置说明

在本设计中,用户通过调用API Gateway中定义好的API,访问AWS上的数据库资源。我们通过定义2个路由,并集成Lambda函数,完成对DynamoDB数据的读/写。

  • GET /items:不需要进行鉴权,可以直接通过API Gateway获取DynamoDB数据。
  • POST /items:需要进行鉴权,通过API Gateway校验请求Token,验证成功后,向DynamoDB写入数据。

2.2 预置条件

  • 创建一个DynamoDB Table,DemoTable。
  • 在AWS API Gateway创建HTTP API,并与Lambda进行集成。

DynomoDB设置

  • 分区键:pk
  • 排序键:sk
  • 其他保留默认配置

API Gateway设置

创建API Gateway,为API Gateway创建2条路由,并关联Lambda函数,列表如下:

路由 Lambda 函数名 Runtime Permission 环境变量
GET /items GetItemsLambda Python 3.9 GetItemsLambdaRole DDB_TABLE
demoDDB
POST /items CreateItemsLambda Python 3.9 CreateItemsLambdaRole DDB_TABLE
demoDDB

 

Lambda函数样例

  • GetItemsLambda
import boto3
import os
import json
import botocore
 
def lambda_handler(event, context):
    try:
        client = boto3.resource("dynamodb")
        table_name = os.environ.get('DDB_TABLE')
        table = client.Table(table_name)
        scanning_result = table.scan()
 
        return {
            'statusCode': 200,
            'body': json.dumps(scanning_result),
            'headers': {
                    'Content-Type': 'application/json'
                },
            'isBase64Encoded': 'false'
        }
    except botocore.exceptions.ClientError as e:
        print(e)
  • GetItemsLambdaRole Policy:
"Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "dynamodb:DescribeTable",
                "dynamodb:Query",
                "dynamodb:Scan"
            ],
            "Resource": [
                "arn:aws:dynamodb:us-east-1:123456789012:table/DemoTable"
            ],
            "Effect": "Allow"
        }
    ]
}
  • CreateItemsLambda:
import json
import datetime
import boto3
import os
import botocore
import uuid
 
def lambda_handler(event, context):
    try:
        client = boto3.client('dynamodb')
        table_name = os.environ.get('DDB_TABLE')
        req_body = json.loads(event["body"])
        req_user = event["requestContext"]["authorizer"]["jwt"]["claims"]["email"]
        ct = datetime.datetime.now()
        client.put_item(
            TableName = table_name,
            Item = {
                'pk': {'S':req_user},
                'sk': {'S':'item#' + str(uuid.uuid4())},
                'title': {'S':req_body["title"]},
                'timeCreated': {'S': ct.isoformat()}
                 
            }
                )
                 
        return {
            'statusCode': 200,
            'body': json.dumps('New Item Created')
        }
    except botocore.exceptions.ClientError as e:
        print(e)
  • CreateItemsLambdaRole Policy:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:Query",
                "dynamodb:GetItem",
                "dynamodb:Scan",
                "dynamodb:ConditionCheckItem",
                "dynamodb:BatchWriteItem",
                "dynamodb:PutItem",
                "dynamodb:UpdateItem",
                "dynamodb:DeleteItem"
            ],
            "Resource": [
                " arn:aws:dynamodb:us-east-1:123456789012:table/DemoTable "
            ],
            "Effect": "Allow"
        }
    ]
}

2.3 Keycloak的设定

2.3.1 Keycloak的安装

关于Keycloak的安装,请参照blog:使用SAML和Keycloak建立AWS SSO登陆Console

2.3.2 Keycloak realm的建立 

Realm 可以理解为域,用于管理用户、用户凭据、角色和用户组。通常我们需要在realm里创建client,不同的应用客户端应在realm中配置不同的client。当进行鉴权时,请求资源的应用客户端会向鉴权服务器请求Auth Code,正如上文中OAuth2.0鉴权流程 第2步所示。为了完成Keycloak realm的建立,我们可以:

  1. 登陆keycloak admin console,点击 “Administration Console”并登陆;

  1. 创建一个realm,并进入到realm中;

2.3.3 Keycloak Client的建立

  1. 在realm中点击“Configure-> Clients-> Create”;

  1. 按照下面的说明输入相关信息;注意选择Standard Flow Enabled,这将会使client的鉴权按照0的“Authorization Code Flow”完成,即OAuth2.0鉴权流程 所示。

注意,如没有特别的需求,尽量避免使用“Implicit Flow”即关闭“Implicit Flow Enabled”。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。这将会把Token直接传给前端,是很不安全的,因此,只能用于一些安全要求不高的场景。

  1. 在“Credentials tab”中,选择Client Id and Secret,此时会生成一串随机字符串作为Secret。在上文中OAuth2.0鉴权流程  第6步中交换Token的过程中,Secret将作为client_secret的值,在应用客户端的POST Body中被发送到Keycloak Client中,以确保向Client交换的Token颁布给了正确的应用客户端。

2.3.4 Keycloak User的建立

  1. 创建测试用的User,点击左侧“Manage-> Users-> Add user”;

  1. 输入User信息;

  1. 设定密码,输入密码,并输入Password Confirmation,点击Reset Password以完成设置。

2.4 API Gateway Authorizer的设定

  1. 进入到上文创建的API Gateway中,由于我们的目的是在创建item时,需要进行API鉴权,因此只在POST /items的路由上附加授权方即可:

  1. 输入相关信息;

  • 身份来源:通常情况下,在请求资源服务器时,会将JWT Token写入到请求头中的Authorization字段,因此可以保留默认。
  • 发布者URL:针对Keycloak为https://{Keycloak_URL}/auth/realms/{realm}/
  • 受众:关联的受众,此处输入account

3. 验证JWT Authorizer

3.1 请求Auth Code

  1. 根据OAuth2.0鉴权流程当用户请求资源时,应用客户端将会向鉴权服务器发送GET请求,以请求Auth Code;
GET https://{Keycloak_URL}/auth/realms/Keycloaksso/protocol/openid-connect/auth?response_type=code&client_id=Keycloak
  1. 在此过程中,鉴权服务器将会返回Keycloak的登陆界面,要求用户输入其用户名密码,在此处,我们输入Keycloak User的建立章节中创建的User的用户名和密码。

  1. 输入正确的用户名和密码后,Keycloak将会通过Query String返回Auth Code,如下图所示;

3.2 交换JWT Token

  1. 接下来,我们通过Postman模仿应用客户端,模拟通过POST Auth Code换取JWT Token的过程;

  • client_id: Keycloak的client ID,为Keycloak Client的建立 章节中创建
  • grant_type: 0的鉴权模式,我们通过authorization code模式鉴权,这也是最常见的模式;
  • client_secret: 用于Client应用客户端的验证;
  • code:Auth Code,由鉴权服务器返回,用于交换Token。
  1. 将Postman的请求用curl实现:
curl --location --request POST 'https://{Keycloak_URL}/auth/realms/Keycloaksso/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: AUTH_SESSION_ID=a0b56dbf-19b0-4d16-b254-c25248834c01.Keycloak-5b7448f8cf-v5wg6; AUTH_SESSION_ID_LEGACY=a0b56dbf-19b0-4d16-b254-c25248834c01.Keycloak-5b7448f8cf-v5wg6; KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyYjQ0M2Q2ZS00MzNiLTQwYTQtYjdlMi03MDk2Mjg1YTJkYmMifQ.eyJjaWQiOiJrZXljbG9hayIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6IioiLCJhY3QiOiJBVVRIRU5USUNBVEUiLCJub3RlcyI6eyJpc3MiOiJodHRwczovL2F1dGguY2lhdGVzdC50b3AvYXV0aC9yZWFsbXMva2V5Y2xvYWtzc28iLCJyZXNwb25zZV90eXBlIjoiY29kZSJ9fQ.5T6tBz-j7vbfzvhHBpPnQ2ebRqYC69gNF-EMlWmsA8Q' \
--data-urlencode 'client_id=Keycloak' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_secret=bba58d29-xxxx-xxxx-xxxx-xxxxxxxxxxxx' \
--data-urlencode 'code=74926370-xxxx-xxxx-xxxx-xxxxxxxxxxxx.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Response

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldU….eyJleHAiOjE2NTYyNDExOTksImlhdCI6MT….m91pmRMmSnA0D37qF4_...",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldU….eyJleHAiOjE2NTYyNDI2OTksImlhdCI6MT….OaDarszhAnyd3NKZTiZ…",
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "aa8b66e0-xxxx-xxxx-xxxx-6ff28b1213d5",
    "scope": "profile email"
}

3.3 向API Gateway 请求创建资源

 

  1. 通过Postman模仿应用客户端,模拟创建item的过程;

  • Authorization:将2中返回的access_token 粘贴在Authorization header中,格式为:Bearer {access_token}
  1. 将Postman的请求用curl实现:
curl --location --request POST 'https://80iiueir8b.execute-api.us-east-1.amazonaws.com/items' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldU….eyJleHAiOjE2NTYyNDExOTksImlhdCI6MT….m91pmRMmSnA0D37qF4_...\
--header 'Content-Type: application/json' \
--data-raw '{"title":"nike high heel"}'

Response

"New Item Created"
  1. 通过浏览器GET /item 查看item是否写入成功:

4. 总结

由于当前Cognito在中国区仍不可用,Keycloak可以作为一个替代方案,完成用户的单点登录和API的鉴权。本blog提供了如何结合API Gateway 的HTTP API,利用Keycloak和JWT进行API鉴权的演示。通过这种方式,应用客户端在请求AWS资源时,需要通过Keycloak服务器进行校验,并换取有效的JWT Token,以获得访问资源的权限。

如有兴趣了解本文提到的更多技术,请参照:

OAuth2.0 Grant Type

Json Web Token

使用 SAML 和 Keycloak 建立 AWS SSO 登录 Console

How to secure API Gateway HTTP endpoints with JWT authorizer

本篇作者

李潇翌

亚马逊云科技专业服务团队安全顾问,负责云安全合规、云安全解决方案等的咨询设计及落地实施,致力于为客户上云提供安全最佳实践,并解决客户上云中碰到的安全需求。