Feature Request: SigV4 Service configurations for Bedrock
Description
Hi,
I am having problems in using aws_sig4. What I am doing is,
- Login via AWS cognito.
- Get user credentials via final result = await cognitoPlugin.fetchAuthSession();
- Setup a button to initiate function _initializeClient() and pass credentials attained by above function.
- My endpoint: bedrock-runtime.us-east-1.amazonaws.com
ISSUE
I am continually getting error "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details"
The CODE FAILS with response code 403. Key function is below. Is there anything I need to fix below or any misunderstanding on my behalf. Please let me know.
Packages Used
aws_common: ^0.7.1
aws_client: ^0.6.0
aws_signature_v4: ^0.6.1
amplify_flutter: ^2.3.0
amplify_auth_cognito: ^2.3.0
Reference function:
Map<String, String> _getCommonHeaders() {
return {
AWSHeaders.contentType: 'application/json', // Set content type as JSON
AWSHeaders.accept: 'application/json',
AWSHeaders.authorization: '',
// AWSHeaders.authorization:
// 'Bearer ${authCredentials.sessionTokens.idToken}', // Use the ID token for authorization
AWSHeaders.securityToken: authCredentials.sessionTokens.sessionToken ??
'', // Use the session token
'X-Amzn-Bedrock-GuardrailIdentifier':
guardRailIdentifier, // Custom header value for guardrail identifier
'X-Amzn-Bedrock-GuardrailVersion': '1', // Version for the guardrail
'X-Amzn-Bedrock-Trace': 'trace', // Optional tracing header
};
}
Future<void> _initializeClient() async {
try {
// Define AWS service
const service = common.AWSService('bedrock');
// Create AWS credentials
final credentials = AWSCredentialsProvider(AWSCredentials(
authCredentials.sessionTokens.accessKeyID,
authCredentials.sessionTokens.secretAccessKey,
authCredentials.sessionTokens.sessionToken,
authCredentials.sessionTokens.expiry,
));
// Initialize the signer
signer = AWSSigV4Signer(
credentialsProvider: credentials,
algorithm: AWSAlgorithm.hmacSha256,
);
final scope = AWSCredentialScope(
region: region,
service: service,
);
// Create the AWSBaseHttpRequest
final awsRequest = common.AWSHttpRequest(
method: common.AWSHttpMethod.post,
uri: //Uri.https('$bedrockEndpoint/model/$modelId/invoke'),
Uri.https(
bedrockEndpoint, // The hostname
'/model/$modelId/invoke', // The path
),
headers: _getCommonHeaders(),
body: json.encode({
'amazon-bedrock-guardrailConfig': {
'tagSuffix': 'string', // Replace with actual tag suffix if needed
},
}).codeUnits,
);
// Sign the request
final signedRequest = await signer.sign(
awsRequest,
credentialScope: scope,
);
debugPrint(
'Response from AWS Bedrock HTTP Method: ${signedRequest.canonicalRequest}');
debugPrint('Response from AWS Bedrock Path: ${signedRequest.path}');
debugPrint(
'Response from AWS Bedrock queryParameters: ${signedRequest.queryParameters}');
debugPrint(
'Response from AWS Bedrock queryParametersAll: ${signedRequest.queryParametersAll}');
debugPrint('Response from AWS Bedrock host: ${signedRequest.host}');
debugPrint(
'Response from AWS Bedrock hasContentLength: ${signedRequest.hasContentLength}');
debugPrint('Response from AWS Bedrock headers: ${signedRequest.headers}');
// Send the request using AWSHttpClient
final operation = signedRequest.send();
// Handle the response
final respBody = await operation.response;
debugPrint('Response from AWS Bedrock: ${respBody.statusCode}');
debugPrint(
'Response from AWS Bedrock: ${await processResponse(respBody.body)}');
if (respBody.statusCode != 200) {
throw HttpRequestException(
message: 'Request failed: ${respBody.statusCode}',
statusCode: respBody.statusCode,
);
}
// Initialize _bedrockClient with the correct client instance
_bedrockClient = bedrock.BedrockRuntime(
//client: client as http.Client,
region: region,
credentials: bedrock.AwsClientCredentials(
accessKey: authCredentials.sessionTokens.accessKeyID,
secretKey: authCredentials.sessionTokens.secretAccessKey,
sessionToken: authCredentials.sessionTokens.sessionToken,
expiration: authCredentials.sessionTokens.expiry,
),
endpointUrl: bedrockEndpoint,
);
} catch (e) {
debugPrint('Authentication request failed: $e');
// Optionally rethrow or handle the error as needed
throw HttpRequestException(
message: 'Failed to initialize client: $e',
);
} finally {
//client.cclose(); // Close the client when done
}
}
Categories
- [ ] Analytics
- [X] API (REST)
- [ ] API (GraphQL)
- [X] Auth
- [ ] Authenticator
- [ ] DataStore
- [ ] Notifications (Push)
- [ ] Storage
Steps to Reproduce
- Login via AWS cognito.
- Get user credentials via final result = await cognitoPlugin.fetchAuthSession();
- Setup a button to initiate function _initializeClient() and pass credentials attained by above function.
- My endpoint: bedrock-runtime.us-east-1.amazonaws.com
Screenshots
NA
Platforms
- [X] iOS
- [ ] Android
- [ ] Web
- [ ] macOS
- [ ] Windows
- [ ] Linux
Flutter Version
latest
Amplify Flutter Version
2.3.0
Deployment Method
Amplify CLI
Schema
NA
Hi @dkliss, thanks for taking the time to submit this.
We will need to try to reproduce the observed behavior.
Can you please provide the full error message and relevant logs?
hi @Equartey
The Error Message is simply below". The error occurs specifically for due to function final operation = signedRequest.send();. The status code received from this function call is 403. And the credentials which are used in this request is attained from await cognitoPlugin.fetchAuthSession().
"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."
If i simply run below without using aws_client: ^0.6.0 &. aws_signature_v4: ^0.6.1 packages, then I still get the same error
" Network error: Authentication request failed: 403 InvalidSignatureException: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details"
Future<void> authenticateUser(AuthCredentials newAuthCredentials) async {
try {
// Reinitialize the Bedrock client with new credentials
//await _initializeClient(); // Ensure _bedrockClient is initialized
_bedrockClient = bedrock.BedrockRuntime(
//client: client as http.Client,
region: region,
credentials: bedrock.AwsClientCredentials(
accessKey: authCredentials.sessionTokens.accessKeyID,
secretKey: authCredentials.sessionTokens.secretAccessKey,
sessionToken: authCredentials.sessionTokens.sessionToken,
expiration: authCredentials.sessionTokens.expiry,
),
endpointUrl: bedrockEndpoint,
);
} catch (e) {
throw HttpRequestException(
message: 'Authentication request failed: $e',
);
}
}
Hi @dkliss, thanks for providing that extra context. We will get back to you with our findings.
Hi @Equartey , I was wondering if any progress on this or if this is still in backlog?
Hi @dkliss, apologies for the delay, we have not got to this yet.
Out of curiosity are you able to make requests to other endpoints using your setup?
Hi @Equartey Thanks for your response.
In us-east-1, the only endpoints listed are below below
static const bedrockEndpointControl = 'bedrock.us-east-1.amazonaws.com';
static const bedrockEndpointRuntime =
'bedrock-runtime.us-east-1.amazonaws.com';
I tried changing mode though from haiku to an aws internal model below using the same approach i mentioned above but no luck.
static const modelId = 'amazon.titan-text-lite-v1';
I then attempted a direct http request but got credential error which i suspect is because of not using aws-sig4.
I am bit unclear if I am missing a key configuration here or am passing correct keys. I expect the authentications keys from await cognitoPlugin.fetchAuthSession(); to authentication client request to AWS but something seem to be not allowing this (403 error always). Seems like aws_sig4 but am not able to get to a conclusion here. So any help is appreciated.
Specs: Http request with bedrockEndpoint: bedrock-runtime.us-east-1.amazonaws.com and modeID: amazon.titan-text-lite-v1'
ERROR from below http request received:
flutter: Response from aws bedrock: {"message":"Invalid key=value pair (missing equal-sign) in Authorization header (hashed with SHA-256 and encoded with Base64): '*************************************************='."}
flutter: Response from aws bedrock: 403
// Authenticate directly via http request
Future<http.Response> authenticateUserviaHttp() async {
final url = Uri.parse('https://$bedrockEndpoint/model/$modelId/invoke');
final response = await http.post(
url,
headers: _getCommonHeaders(),
body: jsonEncode({
'amazon-bedrock-guardrailConfig': {
'tagSuffix': 'string' // Replace with actual tag suffix if needed
}
}),
);
debugPrint('Response from aws bedrock: ${response.body}');
debugPrint('Response from aws bedrock: ${response.statusCode}');
if (response.statusCode != 200) {
throw HttpRequestException(
message: 'Authentication failed: ${response.reasonPhrase}',
statusCode: response.statusCode,
);
}
return response;
}
Map<String, String> _getCommonHeaders() {
return {
AWSHeaders.contentType: 'application/json', // Set content type as JSON
AWSHeaders.accept: '*/*',
AWSHeaders.authorization:
'Bearer ${authCredentials.sessionTokens.idToken}', // Use the ID token for authorization
AWSHeaders.securityToken: authCredentials.sessionTokens.sessionToken ??
'', // Use the session token
'X-Amzn-Bedrock-GuardrailIdentifier':
guardRailIdentifier, // Custom header value for guardrail identifier
'X-Amzn-Bedrock-GuardrailVersion': '1', // Version for the guardrail
'X-Amzn-Bedrock-Trace': 'DISABLED', //Valid Values: ENABLED | DISABLED
};
}
ADD:
Also tried below and same error "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."
I did also cross checked with https://github.com/aws-amplify/amplify-flutter/issues/4506 to ensure signer is same and it does match for signer. Request format is as per format: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html
final awsRequest = common.AWSHttpRequest(
method: common.AWSHttpMethod.post,
uri: Uri.https(
'bedrock-runtime.us-east-1.amazonaws.com',
'/model/amazon.titan-text-express-v1/invoke',
),
headers: _getCommonHeaders(),
body: json.encode({
'amazon-bedrock-guardrailConfig': {
'tagSuffix': 'string',
},
}).codeUnits,
);
And I also tried a totally different endpoint such as userpool below:
// THis is merely used for testing of signer
Future<void> signAndSendRequestToUserPool() async {
// Create the signing scope
final scope = AWSCredentialScope(
region: region,
service: AWSService.cognitoIdentityProvider,
);
// Create AWS credentials
final credentials = AWSCredentialsProvider(AWSCredentials(
authCredentials.sessionTokens.accessKeyId,
authCredentials.sessionTokens.secretAccessKey,
authCredentials.sessionTokens.sessionToken,
authCredentials.sessionTokens.expiration,
));
// Initialize the signer
signer = AWSSigV4Signer(
credentialsProvider: credentials,
algorithm: AWSAlgorithm.hmacSha256,
);
// Create the HTTP request
final request = AWSHttpRequest(
method: AWSHttpMethod.post,
uri: Uri.https('cognito-idp.$region.amazonaws.com', '/'),
headers: const {
AWSHeaders.target: 'AWSCognitoIdentityProviderService.DescribeUserPool',
AWSHeaders.contentType: 'application/x-amz-json-1.1',
},
body: json.encode({
'UserPoolId': userPoolId,
}).codeUnits,
);
// Sign and send the HTTP request
final signedRequest = await signer.sign(
request,
credentialScope: scope,
);
final resp = signedRequest.send();
final respBody = await resp.response;
safePrint(respBody);
debugPrint('Response from AWS Userpool: ${respBody.statusCode}');
debugPrint(
'Response from AWS Userpool: ${await processResponse(respBody.body)}');
}
And for this I got different error as below.
flutter: Response from AWS Userpool: 400
flutter: Response from AWS Userpool: {"__type":"AccessDeniedException","Message":"User: arn:aws:sts::******* is not authorized to perform: cognito-idp:DescribeUserPool on resource: arn:aws:cognito-idp:us-east-1:**************** because no identity-based policy allows the cognito-idp:DescribeUserPool action"}
What seems like, the signer seemed to have worked when used for userpool).
So my guess is signer for bedrock seems to have problem for me for reason I don't know or not sure if I am doing something wrong.
@dkliss thank you for providing these details. we will look into this issue and get back to you with any updates.
Hi @dkliss,
I was able to make a request successfully with the following snippet. The key part that got it working was reusing the S3ServiceConfiguration class when signing the request. I suspect this has something to do with the payload encoding, but need to verify that still. Once we isolate whats required for bedrock, we can expose that as its own class.
In the meantime, my example below should unblock you. Please let us know if you have any other issues.
Dart Bedrock Anthropic Example
// Make a signed request to an Anthropic model on AWS Bedrock.
// Signed by the the AWS SigV4 signer for Dart.
// Note: Request can take several seconds to complete.
Future<void> _sendBedrockRequest() async {
try {
const region = 'YOUR_AWS_REGION';
const modelId = 'anthropic.claude-3-sonnet-20240229-v1:0';
// Fetch Current Auth Session
final cognitoPlugin =
Amplify.Auth.getPlugin(AmplifyAuthCognito.pluginKey);
final session = await cognitoPlugin.fetchAuthSession();
final credentials = session.credentialsResult.value;
// Create AWS credentials
final credentialsProvider = AWSCredentialsProvider(credentials);
// Initialize the signer
final signer = AWSSigV4Signer(
credentialsProvider: credentialsProvider,
);
final scope = AWSCredentialScope(
region: region,
service: const AWSService('bedrock'),
);
// Anthropic Example
final payload = {
"system":
"You are Claude, an AI assistant created by Anthropic to be helpful, harmless, and honest. Your goal is to provide informative and substantive responses to queries while avoiding potential harms.",
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Hello there."},
{
"role": "assistant",
"content": "Hi, I'm Claude. How can I help you?"
},
{"role": "user", "content": "Can you explain LLMs in plain English?"}
]
};
// Create the AWSBaseHttpRequest
final awsRequest = AWSHttpRequest(
method: AWSHttpMethod.post,
uri: Uri.https(
_bedrockEndpoint, // The hostname
'/model/$modelId/invoke', // The path
),
headers: const {
AWSHeaders.contentType: 'application/json',
AWSHeaders.accept: 'application/json',
},
body: jsonEncode(payload).codeUnits,
);
// Sign the request
final signedRequest = await signer.sign(
awsRequest,
credentialScope: scope,
serviceConfiguration: S3ServiceConfiguration(),
);
// Send the request using AWSHttpClient
final operation = signedRequest.send();
// Handle the response
final response = await operation.response;
final body = await _decodeResponse(response);
debugPrint('Response from AWS Bedrock - Status: ${body}');
} catch (e) {
debugPrint('Request failed: $e');
}
}
Future<String> _decodeResponse(AWSBaseHttpResponse response) async {
final sb = StringBuffer()
..writeln(response.statusCode)
..write(await utf8.decodeStream(response.split()));
return sb.toString();
}
Thanks a lot @Equartey for your response.
I can confirm I have received status code below after I change the serviceConfiguration: S3ServiceConfiguration() (I guess we will get bedRock specific serviceConfiguration in future?).
Response from AWS Bedrock - Status: 200
In case it helps, initially, even after adding your proposed serviceConfiguration, I did get response but it also had error 403 because of IAM issues as mentioned here https://repost.aws/knowledge-center/bedrock-invokemodel-api-error.
Error: "AccessDeniedException: An error occurred (AccessDeniedException) when calling the InvokeModel operation: User: <> is not authorized to perform: bedrock:InvokeModel on resource: <> because no identity-based policy allows the bedrock:InvokeModel action.
This was resolved by adding below statement in the role, which was auto created initially by auth (in sandbox, via typescript auth)
{
"Version": "2012-10-17",
"Statement": {
"Sid": "AllowInference",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "arn:aws:bedrock:*::foundation-model/model-id"
}
}
I couldn't find a way to update the policy via code and therefore needed to manually add this to AWS after identifying the role created by amplify sandbox? Not sure if there is any way I can adjust IAM policy within code, for example, in below auth setup.
import { defineAuth, secret } from '@aws-amplify/backend';
export const auth = defineAuth({
loginWith: {
email: true,
I did see https://docs.amplify.aws/gen1/flutter/tools/cli/project/permissions-boundary/ but it gave me an error (even when I have setup a sandbox env already).
amplify env update
🛑 No Amplify backend project files detected within this folder.
Resolution:
Either initialize a new Amplify project or pull an existing project.
- "amplify init" to initialize a new Amplify project
- "amplify pull <app-id>" to pull your existing Amplify project. Find the <app-id> in the AWS Console or Amplify Studio.
@dkliss - You should be able to modify Amplify generated resources with the AWS CDK. Here is a simple example of overriding the password policy. If you have more questions about overriding specific resources I would suggest opening an issue here: https://github.com/aws-amplify/amplify-backend.
I am going to update this to a feature request to expose the appropriate service configuration so that the workaround of using S3 config is not required.
Thanks @Jordan-Nelson. I will have a look at these references. Thanks for looking into this.
Hi everyone, I'm experiencing the same issue described here using the provided example from the repository. Here's the code I'm using:
final host = '$bucketName.s3.$region.amazonaws.com';
final path = '/$objectKey';
final scope = AWSCredentialScope(
region: region,
service: AWSService.s3,
);
final signer = AWSSigV4Signer(
credentialsProvider:
AWSCredentialsProvider(AWSCredentials(accessKey, secretKey)),
);
final serviceConfiguration = S3ServiceConfiguration();
// Creating a pre-signed URL for downloading the file
final urlRequest = AWSHttpRequest.get(
Uri.https(host, path),
headers: {
AWSHeaders.host: host,
AWSHeaders.date: AWSDateTime.now().toString(),
AWSHeaders.contentSHA256: 'UNSIGNED-PAYLOAD',
// AWSHeaders.contentSHA256:
// hex.encode(sha256.convert(utf8.encode('')).bytes),
// AWSHeaders.expires: expirationInSeconds.toString(),
},
);
final signedUrl = signer.presignSync(
urlRequest,
credentialScope: scope,
serviceConfiguration: serviceConfiguration,
expiresIn: Duration(seconds: expirationInSeconds),
);
In my case, the problem occurs with the pre-signed URL, which doesn't work as expected. I followed the example exactly, but I keep receiving the SignatureDoesNotMatch error when trying to access the URL through a browser. I’ve tried verifying the contentSHA256 and other parameters, but without success. However, I've noticed that if I sign the URL and send it using the .send().response method instead of through the browser, it works correctly. This leads me to think that the issue might be related to how the browser handles the pre-signed URL or some detail in the HTTP request. Does anyone have any suggestions on what might be causing this difference in behavior? I'm happy to provide more details if needed. Thanks!
@GregoryPardini Using S3ServiceConfiguration was a workaround since there is no service configuration at the moment for bedrock and the configurations appear to be similar for the two services. If you are finding certain scenarios where this is not working, I would guess that using S3ServiceConfiguration is not a valid work around in all scenarios.
I wanted to add the experience we had with this error message when using the invoke model specifically when the modelId contains a ':'. We have been able to make calls to claude-v2 and claude-instant but are wanting to specifically use the Messages API which is only available starting with claude 3. I should mention that claude 2.1 is not successfully reachable either.
Error: ```"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."````
Calling Method:
Future<String> invokeBedrockModel(String modelId, String prompt,
String accept, AWSCredentialsProvider credentials) async {
final endpoint = "$service.$region.${clientConfig.config.awsBaseUrl}";
final path = 'model/$modelId/invoke';
final awsSigner = AWSSigV4Signer(
credentialsProvider: credentials,
);
final request = AWSHttpRequest(
method: AWSHttpMethod.post,
uri: Uri.https(endpoint, path),
headers: {
'Content-Type': 'application/json',
'Accept': accept,
},
body: json.encode({
'prompt': "Human: $prompt\n\nAssistant: ",
'max_tokens_to_sample': 3000,
'temperature': 0.5,
}).codeUnits,
);
final signedRequest = await awsSigner.sign(
request,
credentialScope:
AWSCredentialScope(region: region, service: AWSService.bedrock),
);
//final response = await signedRequest.send().response;
// final response = await http.post(signedRequest.uri, headers: signedRequest.headers, body: signedRequest.body);
final response = await signedRequest.send().response;
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(await response.decodeBody());
return jsonResponse['completion']; // Extract the model's response
} else {
debugPrint(await response.decodeBody());
throw Exception('Failed to invoke Bedrock model: ${response.statusCode}');
}
}```
The successful modelId: `const String modelId = 'anthropic.claude-v2';`.
The unsuccessful: `const String modelId = 'anthropic.claude-3-haiku-20240307-v1:0';`and `const String modelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0';`, and `anthropic.claude-v2:1`
@ethom7 Thanks for the extra context. It appears each model my require a different signing approach. We will keep this in mind when we add support.
@ethom7 Thanks for the extra context. It appears each model my require a different signing approach. We will keep this in mind when we add support.
Hi @Equartey, when is this support as well as proper service configuration for bedrock is going to be added? Is this something being considered or is it a long wait?
Hi @dkliss, this is still in our backlog so we do not have an ETA yet. When that changes we will notify y'all here.
FYI: 👍 reactions to this issue help us decide when to address a request.
Just adding my $0.02 to this conversation: it appears that this is a problem whenever the model ID has a colon in it. If you replace the colon with a period (or anything else that doesn't require URL-encoding), then the request signature is valid and you get a different error about the model ID not being valid.
I have noticed a similar error when using the Dart Sigv4 library with Route53, and I believe that payload also has characters that ought to be URL-encoded.
So, it appears to me that the problem is with how the Sigv4 library handles characters that are involved with URL-encoding or URL-decoding.