使用Amazon Cognito为AI代理注入用户上下文 | AWS安全博客
Amazon Cognito是一项托管的客户身份和访问管理(CIAM)服务,可为Web和移动应用程序提供无缝的用户注册和登录功能。通过用户池,Amazon Cognito提供具有强身份验证功能的用户目录,包括通行密钥、与外部身份提供商(IdP)的联合以及用于安全机器对机器(M2M)授权的OAuth 2.0流程。
Amazon Cognito颁发标准的JSON Web令牌(JWT),并支持使用预令牌生成Lambda触发器自定义身份和访问令牌以进行用户身份验证。Amazon Cognito扩展了令牌自定义功能,以支持M2M的访问令牌自定义以及在M2M授权期间从客户端传递元数据的能力。应用程序构建者可以使用这两个功能来支持多种用例,包括基于唯一运行时策略、权限、环境或传递的元数据自定义访问令牌。这可以简化和丰富M2M身份验证和授权场景,并为新兴用例(如AI代理的身份和访问管理)开辟新的可能性。
本文演示了Amazon Cognito如何通过为支持OAuth 2.0的资源服务器生成用户上下文化的访问令牌,使AI代理能够代表用户执行授权操作。AI代理代表一类需要强大身份管理和精确访问控制的自主服务,尤其是在代表用户行事时。通过使用具有访问令牌自定义功能的Amazon Cognito客户端凭证流,您可以为AI代理建立不同的身份,这些身份携带有关其功能、访问范围和预期用例的关键信息。这种方法为更安全、可审计的AI代理操作奠定了基础,同时在其授权活动周围保持清晰的边界。
AI代理的身份可以在Amazon Cognito中表示为应用程序客户端。AI代理通过OAuth 2.0客户端凭证授权获取访问令牌(JSON Web令牌(JWT))。此JWT可以自定义为包含代表AI代理所代表的已认证人类用户的声明。然后,该令牌可用于授权访问其他服务,这些服务通过信任令牌的颁发者和受众与Amazon Cognito用户池建立了信任关系。例如,此第三方服务可以是代表用户行事的声明处理器、旅行社服务或调度服务。本文的重点是使用Amazon Cognito为AI代理构建基础构建块,以及如何获取带有用户上下文的定制访问令牌。
解决方案概述和参考架构
查看示例架构(图1),用户使用Amazon Cognito用户池登录到Web或移动应用程序,并将用户的令牌返回给客户端。在这里,应用程序可以是一个使用Amazon Bedrock代理的无服务器数字助手,需要收集和处理驻留在第三方跨域服务中的数据。AI代理通过执行OAuth 2.0客户端凭证授权来获取自己的访问令牌,同时使用aws_client_metadata请求参数传递用户的访问令牌作为上下文。AI代理接收用户上下文化的访问令牌,并调用外部、第三方或跨域服务,该服务信任从Amazon Cognito用户池颁发的AI代理访问令牌的颁发者和受众。跨域服务可以获取JSON Web密钥集(JWKS)来验证令牌并提取声明,呈现AI代理以及最重要的底层用户。授权在跨域服务内使用定制访问令牌的声明进行,对于细粒度授权,使用Amazon Verified Permissions。有关此示例的详细流程,请参见图1。
图1:AI代理身份参考架构
- 用户通过客户端导航到应用程序。
- 用户没有现有的会话或令牌,因此开始与Amazon Cognito用户池进行用户身份验证流程。
- 成功登录后,Amazon Cognito将用户的访问、ID和刷新令牌返回给客户端。
- 当用户通过应用程序与AI代理交互时,客户端将用户的访问令牌发送到Amazon API Gateway端点。
- API网关与使用Amazon Bedrock代理的AI代理集成。例如,这可以使用多个与Amazon Bedrock知识库或检索增强生成(RAG)过程交互的AWS Lambda函数。
- AI代理使用OAuth 2.0客户端凭证授权从Amazon Cognito用户池获取自己的访问令牌。在步骤1中获取的用户访问令牌与令牌请求一起在
aws_client_metadata请求参数中发送。
注意:您可以使用不同的Amazon Cognito用户池进行用户身份验证和代理(机器)身份验证。这促进了分离,并提供了根据需要为每个用户池应用不同设置和控制以满足安全要求的能力。
- Amazon Cognito验证来自AI代理的客户端ID和密钥,并调用预令牌生成Lambda触发器以自定义AI代理的访问令牌。
注意:在预令牌生成Lambda触发器中,使用aws-jwt-verify库验证用户的访问令牌,然后向AI代理返回定制的访问令牌。
- 定制访问令牌返回给AI代理,包括代表用户的定制声明。
- AI代理使用自己的访问令牌调用跨域服务,以代表用户执行请求的操作。例如,这可以是第三方预订系统或照片共享服务。
- 跨域服务中的资源服务器验证来自AI代理的访问令牌是否有效。资源服务器必须预先配置为信任颁发代理访问令牌的用户池。
- 粗粒度和细粒度授权可以在服务代码本地进行,也可以使用Verified Permissions。
- 如有必要,来自跨域服务的响应流回AI代理。
- 如有必要,来自AI代理的响应返回给用户应用程序或客户端。
- 整个流程中发生的操作记录在AWS CloudTrail中,提供端到端的日志记录和审计。
实现细节
让我们更深入地了解此场景的三个核心组件:
- AI代理获取自己的OAuth 2.0访问令牌
- 用于使用用户上下文丰富AI代理访问令牌的Amazon Cognito预令牌生成Lambda触发器
- 执行细粒度授权的跨域资源服务器
AI代理
图2:AI代理通过API Gateway从前端应用程序获取用户访问令牌
此解决方案中使用Amazon Bedrock Agents,配置了使用Lambda的自定义编排。当应用程序与Amazon Bedrock代理交互时,自定义编排器启动,代理将用户的访问令牌作为自定义编排的一部分传递给Lambda函数(如图2所示)。Lambda函数验证用户的令牌以确保其未过期且未被篡改。此自定义编排器开始代理获取自己的OAuth访问令牌并代表用户访问下游和跨域资源的过程。人类用户的访问令牌通过客户端从应用程序调用中包含。要了解更多关于Amazon Bedrock Agents自定义编排器的信息,请参阅开始使用Amazon Bedrock Agents自定义编排器。以下是通过API Gateway REST API提供的人类用户解码访问令牌的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"sub": "user-identity-4e4c-example-7cede8e609a2",
"cognito:groups": ["exampleChatApplicationAccess"],
"iss": "https://cognito-idp.<region>.amazonaws.com/<region>_example",
"version": 2,
"client_id": "1example23456789",
"origin_jti": "",
"token_use": "access",
"scope": "openid profile email",
"auth_time": 499192140,
"exp": 1445444940,
"iat": 499192140,
"jti": "",
"username": "my-example-username"
}
|
以下是AI代理可用于从Amazon Cognito获取自己的访问令牌的Node.js代码示例。这可以是Amazon Bedrock代理自定义编排的Lambda函数部分。注意设置了clientMetadata变量,它将使用aws_client_metadata请求参数传递给Cognito的/token端点。此请求参数是提供用户访问令牌的地方。在以下代码示例中,您将找到一个名为callerApp的属性,设置为ExampleChatApplication,作为应用程序的唯一标识符。callerApp值在解决方案的后端预先配置。此唯一应用程序标识符包含在代理的定制访问令牌中,并用于后续的额外授权检查。作为安全最佳实践,应使用AWS Secrets Manager存储客户端ID和客户端密钥,并在运行时获取这些凭据。作为安全最佳实践,应在将用户访问令牌传递给AI代理后端之前进行验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
async function getAccessToken() {
const clientId = 'exampleAiAgentClientId'; // use Secrets Manager
const clientSecret = 'exampleAiAgentClientSecret'; // use Secrets Manager
const tokenEndpoint = 'https://mydomain.auth.<region>.amazoncognito.com/oauth2/token';
const scope = 'crossDomainService/read userData/read';
const clientMetadata = '{"onBehalfOfToken":"<HUMAN-USER-ACCESS-TOKEN>", "callerApp":"ExampleChatApplication"}';
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const body = new URLSearchParams({
grant_type: 'client_credentials',
scope,
aws_client_metadata: clientMetadata
});
const res = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Token request failed: ${res.status} ${error}`);
}
const { access_token } = await res.json();
console.log('Access Token:', access_token);
return access_token;
}
getAccessToken().catch(err => console.error('Error:', err.message));
|
仅当客户端ID和密钥正确且提供的用户访问令牌有效时,才会返回AI代理的访问令牌。但是,在返回之前,AI代理的访问令牌由Amazon Cognito预令牌生成Lambda触发器进行定制。
Amazon Cognito预令牌生成Lambda触发器
图3:使用Cognito预令牌生成Lambda触发器定制AI代理访问令牌
在AI代理的操作使用有效的客户端ID和密钥调用Amazon Cognito的/token端点后,Cognito调用预令牌生成Lambda触发器。以下是一个示例Lambda函数,它获取aws_client_metadata请求参数,其中包含用户的访问令牌和用户身份验证时定义的callerApp属性。在以下Lambda函数中,验证从用户提供的访问令牌(如图3所示)。使用aws-jwt-verify库验证令牌未过期、令牌未被篡改(通过验证签名),并确保提供了访问令牌。Lambda函数还预先配置为接受来自特定颁发者和受众的用户令牌,这可以防止恶意上下文注入风险。这也是执行额外授权的机会。例如,检查用户是否是某些组的成员。
验证令牌后,Lambda函数定制要返回给AI代理的访问令牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
import { CognitoJwtVerifier } from "aws-jwt-verify";
// Initialize the JWT verifier to verify the user’s access token
// Provide the user pool ID, token use, and client ID
const jwtVerifier = CognitoJwtVerifier.create({
userPoolId: process.env.USER_POOL_ID, // user pool for user authentication
clientId: process.env.CLIENT_ID,
// groups: "exampleChatApplicationAccess", // optional group membership authorization
tokenUse: 'access'
});
export const handler = async function(event, context) {
try {
const onBehalfOfToken = event.request.clientMetadata?.onBehalfOfToken || '';
// It’s recommended that the provided “callerApp” value from the application is authorized for use with the app client for the AI agent
const callerApp = event.request.clientMetadata?.callerApp || '';
// The below console log will display the authenticated user’s JWT
// Keep this logging with caution in a production environment
console.log('Original event:', event);
// Verify the access token from the human user
// You could optionally also perform some authorization checks here as well
// Example: check for the membership of a group
let decodedJWT;
if (onBehalfOfToken) {
try {
decodedJWT = await jwtVerifier.verify(onBehalfOfToken);
console.log('Decoded JWT:', decodedJWT);
} catch (err) {
console.error('Token verification failed:', err);
throw new Error('Token verification failed');
}
}
// Create the onBehalfOf claim structure
const behalfOfClaim = decodedJWT ? {
sub: decodedJWT.sub,
username: decodedJWT.username,
groups: decodedJWT['cognito:groups'] || []
} : {};
// Customized token returned to client
event.response = {
"claimsAndScopeOverrideDetails": {
"accessTokenGeneration": {
"claimsToAddOrOverride": {
"onBehalfOf": behalfOfClaim,
"callerApp": callerApp
},
}
}
};
return event;
} catch (error) {
console.error('Error in Lambda execution:', error);
throw error;
}
};
|
注意在前面的Lambda函数中,在event.response中动态创建了两个自定义声明:onBehalfOf和callerApp。onBehalfOf声明包含从人类用户访问令牌中提取的嵌套声明。callerApp从前端应用程序传递,并与用户访问令牌一起提供。建议对callerApp值也进行一些自定义逻辑验证,以增加额外的保护层。返回的AI代理访问令牌将如下所示的JWT。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
{
"sub": "agent-identity-4e4c-example-7cede8e609a2",
"onBehalfOf": {
"sub": "user-identity-4e4c-example-7cede8e609a2",
"username": "my-example-username",
"groups": ["readaccess"]
},
"iss": "https://cognito-idp..amazonaws.com/_example",
"version": 2,
"client_id": "1example23456789",
"callerApp": "ExampleChatApplication",
"token_use": "access",
"scope": "crossDomainService123/read userData/read",
"auth_time": 499192140,
"exp": 1445444940,
"iat": 499192140,
"jti": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}
|
跨域资源服务器授权检查
此时,如图4所示,人类用户已成功验证到Web应用程序,人类用户的访问令牌作为上下文发送到后端,AI代理获取了自己的包含人类用户上下文的定制访问令牌,现在代理准备调用外部跨域服务。
图4:跨域资源服务器使用Amazon Verified Permissions执行细粒度授权
如图4所示,跨域服务是资源服务器,因此需要执行授权检查。对于此示例,我们将保持简单,并确保验证三个核心事项:
- AI代理的OAuth访问令牌有效
- AI代理被授权访问此服务
- AI代理被授权与用户数据交互
根据您的用例和要求,您可能还需要验证在AI代理代表用户行事之前是否已获得用户同意。最终,您希望验证AI代理可以代表用户访问用户数据,并且仅用于用户已提供同意的目的。
对于令牌验证,再次使用aws-jwt-verify库。以下是验证AI代理访问令牌的Node.js示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import { CognitoJwtVerifier } from "aws-jwt-verify";
// add custom logic to verify that AI agent is authorized to perform this action on behalf of the user
// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
userPoolId: "<user_pool_id>", // user pool for AI agent authentication
tokenUse: "access",
clientId: "<client_id>",
});
try {
const payload = await verifier.verify(
"eyJraWQeyJhdF9oYXNoIjoidk..." //this will be the AI agent's access token
);
console.log("Token is valid. Payload:", payload);
} catch {
console.log("Token not valid!");
}
|
使用Verified Permissions进行细粒度授权
作为安全最佳实践,应使用Verified Permissions执行基于身份的细粒度授权的零信任原则。前面的Node.js代码示例是AI代理访问令牌的基本验证,可以在应用程序逻辑中进行。与其将授权逻辑保留在资源服务器内,不如使用Verified Permissions将授权策略卸载到托管服务。以下是此用例的示例Cedar策略。
1
2
3
4
5
6
7
8
9
10
11
|
permit(
principal == Agent::"agent-identity-4e4c-example-7cede8e609a2",
action == Action::"readOnly",
resource == Resource::"crossDomainService123::userData"
)
when {
resource.scope == Scope::"crossDomainService123/read" &&
resource.owner == User::" user-identity-4e4c-example-7cede8e609a2" &&
context.onBehalfOf.sub == " user-identity-4e4c-example-7cede8e609a2" &&
context.callerApp == "ExampleChatApplication"
};
|
使用前面的Cedar策略示例,您允许AI代理从crossDomainService123资源读取userData。这仅在AI代理的访问令牌包含crossDomainService/read范围且资源所有者和onBehalfOf用户(来自访问令牌)相同(在这种情况下是人类用户)时才被允许。策略中还有一个额外的when子句,以确保此交互从ExampleChatApplication启动。
跨域资源服务器将使用AI代理的访问令牌并调用Verified Permissions的IsAuthorizedWithToken API。要了解更多信息,请参阅使用Amazon Verified Permissions和Amazon Cognito简化细粒度授权。
以下是使用AWS SDK for JavaScript v3从Verified Permissions使用IsAuthorizedWithToken API的Node.js示例。
1
2
3
4
5
6
7
8
9
10
|
import { VerifiedPermissionsClient, IsAuthorizedWithTokenCommand } from "@aws-sdk/client-verifiedpermissions";
const client = new VerifiedPermissionsClient({ region: "<region>" });
// Dynamically provided token
const jwtToken = "eyJraWQiOiJrMWtleSIsInR..."; //AI agent's access token
async function checkAccess() {
const input = {
policy
|