当前随着企业业务的蓬勃增长,越来越多系统迁移在了云上。而各种云的兴起,许多复杂的业务场景需要处在一个多云的环境中。如何将多云进行集成,成了一个普遍且长期的问题。
在我们的客户中,存在混合使用AWS和Azure两种云服务的情况。由于历史原因,某一客户的认证系统使用了Azure AD,而大部分业务系统在AWS上。客户希望将在AWS上一个内部API服务与Azure AD集成。
本方案将讨论如何集成Azure AD与API gateway的方案,主要讲解AWS端的详细配置。设计架构如下:
方案架构
在这个架构中,主要分登陆, 请求API,登出三个流程
- 登入:前端引用Azure AD登陆页面,并获取令牌,保存在浏览器缓存中
- 请求API:请求通过AWS WAF的验证,访问Amazon API Gateway。请求头中携带Authorization的令牌。Amazon API Gateway配置的Authorizer会验证令牌的有效性。验证通过后调用封装好的 Amazon Lambda (无服务函数),处理请求
- 登出:前端引用Azure AD登出页面,清除缓存中的令牌。
这里面的关键组件是:
- 前端单页应用程序(Single-page application):在浏览器中运行的程序,使用Microsoft 身份验证库 (MSAL) 的JS版本。它有封装好的登陆和登出API,访问Azure AD获取相应的token。
- 防火墙:使用AWS WAF,对访问的流量进行白名单控制和过滤。
- API:使用Amazon API Gateway封装和发布API。
- 验证器:用于验证对API请求时候,token的有效性Amazon Lambda 函数(Authorizer)。
准备工作
- 有一个Azure账号,在Azure 里注册一个App,并且配置好回调URI地址,类型选择SPA(Single-page application)。记录好注册成功App的Application (client) ID。
- 在Azure App中,创建组和登陆用户,用于后面的登陆。
- 在Azure Portal的 Quickstart 中有对应的 (Single-page application) SPA的demo,下载下来供参考。或是自己引用MSAL包,写好相关的登陆,请求和登出功能的前端代码。
- 注册并申请一个AWS 账号。
- 如果使用中国区的AWS服务,国内的Amazon API Gateway 443,80 的端口都是关闭的,需要先申请备案才能开放。否则会出现403的错误。
步骤
创建Amazon API Gateway
创建一个REST的API,接收客户端的请求。参考在Amazon API Gateway中创建一个REST API。
配置Amazon Lambda代理
创建一个Amazon Lambda代理集成,参考如何创建Amazon Lambda 代理集成。用于处理请求,封装业务逻辑。出于测试考虑,可以配置一个简单的Hello from Lambda返回。
配置WAF
关于如何创建AWS WAF,请参考 Getting started with AWS WAF 。创建好之后,在API Gateway中,配置Web Application Firewall (WAF)。
创建Lambda认证函数
参考 如何创建一个API Gateway Lambda Authorizer,此处创建一个token based 的Lambda 函数,python版本3.9 。函数内容如下。其中:
- 返回的Policy json,Deny代表拒绝访问,Allow代表通过验证。
- Azure AD 的token有Access token和ID token两种。该函数验证的是ID token。
- Lambda函数配置一个环境变量:APP_CLIENT_ID,来自前面准备工作中记录的Azure app client id。
- Lambda函数主要逻辑:验证token签名的有效;token是否过期;token中的app client id与配置的是否一致
import json
import logging
import os
import time
import traceback
import urllib.request
import jwt as JWT
from jose import jwk, jwt
from jose.utils import base64url_decode
logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
app_client_id = os.getenv("APP_CLIENT_ID")
keys_url = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
with urllib.request.urlopen(keys_url) as f:
response = f.read()
keys = json.loads(response.decode("utf-8"))["keys"]
def lambda_handler(event, context):
if event.get("type") != "TOKEN":
return json.dumps({})
print(event)
try:
req_token = event.get("authorizationToken")
method = event["methodArn"]
empty_response = {
"principalId": "Unknown",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{"Action": "execute-api:Invoke", "Effect": "Deny", "Resource": "*"}
],
},
}
headers = JWT.get_unverified_header(req_token)
kid = headers["kid"]
# search for the kid in the downloaded public keys
key_index = -1
for i in range(len(keys)):
if kid == keys[i]["kid"]:
key_index = i
break
if key_index == -1:
print("Public key not found in keys")
return empty_response
public_key = jwk.construct(keys[key_index], algorithm="RS256")
# get the last two sections of the token, message and signature (encoded in base64)
message, encoded_signature = str(req_token).rsplit(".", 1)
# decode the signature
decoded_signature = base64url_decode(encoded_signature.encode("utf-8"))
# verify the signature
if not public_key.verify(message.encode("utf8"), decoded_signature):
print("Signature verification failed")
return empty_response
print("Signature successfully verified")
claims = jwt.get_unverified_claims(req_token)
print("claims = ", claims)
# Verify the token expiration
if time.time() > claims["exp"] or claims["aud"] != app_client_id:
print("Token is expired or was not issued for this client")
return {
"principalId": claims["preferred_username"],
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{"Action": "execute-api:Invoke", "Effect": "Deny", "Resource": method}
],
},
}
success_resp = {
"principalId": claims["preferred_username"],
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{"Action": "execute-api:Invoke", "Effect": "Allow", "Resource": method}
],
},
}
print(success_resp)
return success_resp
except Exception as e:
print("........Error.... !!!!!!....... ", e)
logger.critical(traceback.format_exc(limit=25))
return
该函数依赖如下python包:
需要将依赖打包并导入才能执行。可以创建一个小型的AWS EC2 服务器,并选择Amazon Linux镜像,里面打包出来的python库会与Lambda兼容。创建好的EC2后,在其中执行如下命令。
pip3 install --target ./python pyjwt python-jose
zip -r auth-lib.zip ./python
导出创建好的zip文件,在lambda的console中创建lambda的layer,上传打包好的zip文件。更多的细节请参考 如何创建和分享Lambda Layer
在lambda函数页面点击Layers框,然后通过ARN的方式添加我们创建的Layer。
配置完成后。可以测试lambda是否正常工作。参考在控制台中测试lambda。
配置Authorizer
此时将创建好的认证lambda作为Authorizer,配置进API Gateway中。参考在API Gateway控制台中配置一个Lambda authorizer
测试API
我们在前面的准备工作中有下载MSAL的Demo,可以改写其中MSAL的acquireTokenSilent方法,返回结果中获取id token。
msalInstance.acquireTokenSilent(request)
.then((response) => {
var idToken = response.idToken;
})
然后我们使用postman,设置Authorization头,值为获取的Id token,请求API的地址测试。
如果Body返回正常结果则成功,如果返回Deny或是null,则登陆失败。
结论
本文阐述一个集成Azure AD与Amazon API Gateway的简单方案。如果需要更完整的认证功能,我们可以扩充该方案,包括后台增加token的生命周期管理功能,或使用MSAL的web应用程序版本等。
本篇作者