Closed ghost closed 8 years ago
If you need to get presigned urls, use the AWSS3PreSignedURLBuilder. More info is in the docs here http://docs.aws.amazon.com/mobile/sdkforios/developerguide/s3transfermanager.html As long as you have a credentials provider hooked to user pools it should satisfy all of your needs.
We cannot use S3 Presigned URLs for multiple reasons:
This is why we want to arbitrarily sign HTTP requests to AWS.
S3 has slightly different signature protocol than the rest of the aws services, take a look @ https://github.com/aws/aws-sdk-ios/blob/master/AWSCore/Authentication/AWSSignature.m#L178. which is the specific part which does the signing for S3 requests.
I was not aware of AWSSignature
. Is there any way we can leverage that to accomplish what we want?
you should be able to use that code verbatim. Although you may or may not want the streaming bit
Copying the code verbatim did not work. I had to modify it to include the x-amz-date
header, and also include the host (which I just grabbed from the library I linked above). Here is what the function looks like after my modifications:
+ (NSString *)signS3RequestV4:(NSMutableURLRequest *)urlRequest
credentials:(AWSCredentials *)credentials
inRegion:(UAAWSRegion)region {
if ( [urlRequest valueForHTTPHeaderField:@"Content-Type"] == nil) {
[urlRequest addValue:@"binary/octet-stream" forHTTPHeaderField:@"Content-Type"];
}
// fix query string
// @"?location" -> @"?location="
// NSString *subResource = request.subResource;
// if (nil != subResource
// && [subResource length] > 0
// && [subResource rangeOfString:@"="].location == NSNotFound) {
// [request setSubResource:[subResource stringByAppendingString:@"="]];
// [request.urlRequest setURL:request.url];
// }
NSDate *date = [NSDate aws_clockSkewFixedDate];
NSString *dateStamp = [date aws_stringValue:AWSDateShortDateFormat1];
//NSString *dateTime = [date aws_stringValue:AWSDateAmzDateFormat];
// Set the X-Amz-Date header
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyyMMdd'T'HHmmss'Z'"];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
NSString *xAmzDate = [dateFormatter stringFromDate:date];
[urlRequest setValue:xAmzDate forHTTPHeaderField:@"X-Amz-Date"];
// Make sure we manually set the host header
[urlRequest setValue:urlRequest.URL.host forHTTPHeaderField:@"Host"];
NSString *scope = [NSString stringWithFormat:@"%@/%@/%@/%@", dateStamp, [NSString UA_regionStringForRegionValue:region],
[urlRequest.URL UA_AWSServiceName], AWSSignatureV4Terminator];
NSString *signingCredentials = [NSString stringWithFormat:@"%@/%@", credentials.accessKey, scope];
// compute canonical request
NSString *httpMethod = urlRequest.HTTPMethod;
// URL.path returns unescaped path
// For S3, url-encoded URI need to be decoded before generate CanonicalURI, otherwise, signature doesn't match occurs. (I.e. CanonicalURI for "/ios-v2-test-445901470/name%3A" will still be "/ios-v2-test-445901470/name%3A". "%3A" -> ":" -> "%3A")
NSString *cfPath = (NSString*)CFBridgingRelease(CFURLCopyPath((CFURLRef)urlRequest.URL)) ;
NSString *path = [cfPath aws_stringWithURLEncodingPath];
if (path.length == 0) {
path = [NSString stringWithFormat:@"/"];
}
NSString *query = urlRequest.URL.query;
if (query == nil) {
query = [NSString stringWithFormat:@""];
}
// Compute contentSha256
NSString *contentSha256;
NSInputStream *stream = [urlRequest HTTPBodyStream];
NSUInteger contentLength = [[urlRequest allHTTPHeaderFields][@"Content-Length"] integerValue];
if (nil != stream) {
contentSha256 = @"STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
[urlRequest setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[AWSS3ChunkedEncodingInputStream computeContentLengthForChunkedData:contentLength]]
forHTTPHeaderField:@"Content-Length"];
[urlRequest setValue:nil forHTTPHeaderField:@"Content-Length"]; //remove Content-Length header if it is a HTTPBodyStream
[urlRequest setValue:@"Chunked" forHTTPHeaderField:@"Transfer-Encoding"];
[urlRequest addValue:@"aws-chunked" forHTTPHeaderField:@"Content-Encoding"]; //add aws-chunked keyword for s3 chunk upload
[urlRequest setValue:[NSString stringWithFormat:@"%lu", (unsigned long)contentLength] forHTTPHeaderField:@"x-amz-decoded-content-length"];
} else {
contentSha256 = [AWSSignatureSignerUtility hexEncode:[[NSString alloc] initWithData:[AWSSignatureSignerUtility hash:[urlRequest HTTPBody]] encoding:NSASCIIStringEncoding]];
//using Content-Length with value of '0' cause auth issue, remove it.
if (contentLength == 0) {
[urlRequest setValue:nil forHTTPHeaderField:@"Content-Length"];
} else {
[urlRequest setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[[urlRequest HTTPBody] length]] forHTTPHeaderField:@"Content-Length"];
}
}
//[request.urlRequest setValue:dateTime forHTTPHeaderField:@"X-Amz-Date"];
[urlRequest setValue:contentSha256 forHTTPHeaderField:@"x-amz-content-sha256"];
//Set Content-MD5 header field if required by server.
if (([ urlRequest.HTTPMethod isEqualToString:@"PUT"] && ([[[urlRequest URL] query] hasPrefix:@"tagging"] ||
[[[urlRequest URL] query] hasPrefix:@"lifecycle"] ||
[[[urlRequest URL] query] hasPrefix:@"cors"]))
|| ([urlRequest.HTTPMethod isEqualToString:@"POST"] && [[[urlRequest URL] query] hasPrefix:@"delete"])
) {
if (![urlRequest valueForHTTPHeaderField:@"Content-MD5"]) {
[urlRequest setValue:[NSString aws_base64md5FromData:urlRequest.HTTPBody] forHTTPHeaderField:@"Content-MD5"];
}
}
NSMutableDictionary *headers = [[urlRequest allHTTPHeaderFields] mutableCopy];
NSString *canonicalRequest = [AWSSignatureV4Signer getCanonicalizedRequest:httpMethod
path:path
query:query
headers:headers
contentSha256:contentSha256];
AWSLogVerbose(@"Canonical request: [%@]", canonicalRequest);
NSString *stringToSign = [NSString stringWithFormat:@"%@\n%@\n%@\n%@",
AWSSignatureV4Algorithm,
[urlRequest valueForHTTPHeaderField:@"X-Amz-Date"],
scope,
[AWSSignatureSignerUtility hexEncode:[AWSSignatureSignerUtility hashString:canonicalRequest]]];
AWSLogVerbose(@"AWS4 String to Sign: [%@]", stringToSign);
NSData *kSigning = [AWSSignatureV4Signer getV4DerivedKey:credentials.secretKey
date:dateStamp
region:[NSString UA_regionStringForRegionValue:region]
service:[urlRequest.URL UA_AWSServiceName]];
NSData *signature = [AWSSignatureSignerUtility sha256HMacWithData:[stringToSign dataUsingEncoding:NSUTF8StringEncoding]
withKey:kSigning];
NSString *signatureString = [AWSSignatureSignerUtility hexEncode:[[NSString alloc] initWithData:signature
encoding:NSASCIIStringEncoding]];
NSString *authorization = [NSString stringWithFormat:@"%@ Credential=%@, SignedHeaders=%@, Signature=%@",
AWSSignatureV4Algorithm,
signingCredentials,
[AWSSignatureV4Signer getSignedHeadersString:headers],
signatureString];
if (nil != stream) {
AWSS3ChunkedEncodingInputStream *chunkedStream = [[AWSS3ChunkedEncodingInputStream alloc] initWithInputStream:stream
date:date
scope:scope
kSigning:kSigning
headerSignature:signatureString];
[urlRequest setHTTPBodyStream:chunkedStream];
}
[urlRequest setValue:authorization forHTTPHeaderField:@"Authorization"];
return authorization;
}
However, even after doing this, I am still getting the same problem:
2016-09-22 12:29:14.818 CognitoSpike[39060:1862304] Headers: {
Authorization = "AWS4-HMAC-SHA256 Credential=ASIAJEI42VHAPROHOZZA/20160922/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=dc64e560a6534bfa28e861f238d1f9f8a1ae13830d143fa3585a03ae2ec67e43";
"Content-Type" = "binary/octet-stream";
Host = "s3.amazonaws.com";
"X-Amz-Date" = 20160922T172914Z;
"x-amz-content-sha256" = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
}
2016-09-22 12:29:15.032 CognitoSpike[39060:1863006] Downloaded data: '<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>InvalidAccessKeyId</Code><Message>The AWS Access Key Id you provided does not exist in our records.</Message><AWSAccessKeyId>ASIAJEI42VHAPROHOZZA</AWSAccessKeyId><RequestId>9394FED7FC3F16C1</RequestId><HostId>+VHHuPKnmt4LgQN535lCC2P/zxcouhTDP1V/fldL8fw3s7rBJW24Pg2hvwlOGBhiqGtt5wkY/MA=</HostId></Error>'
The credentials are the ones we get from -credentials
we call on the instance of AWSCognitoCredentialsProvider
we have and have hooked up to the User Pool.
Should we be getting credentials from somewhere else?
no the credentials from your instance of credential provider should do the trick
Hm. Any idea why they're not, then?
Sniffing the difference between using the transfer manager and our raw signer, it looks like the transfer manager also includes an x-amz-security-token header.
You should also add the session header https://github.com/aws/aws-sdk-ios/blob/master/AWSCore/Authentication/AWSSignature.m#L160
Adding the session header looks like it worked. Let me run a few tests to make sure.
This does appear to work now. Thank you for your help.
It is worth future reference and search engines to be clear:
If you get the error message
The AWS Access Key Id you provided does not exist in our records.
Then you should make sure you are setting all of the headers correctly, especially making sure there is a session key.
I am closing this issue.
This is in Xcode 7.3.1 (7D1014) with CocoaPods 0.39.0 on OS X 10.11.6. We are writing exclusively in Objective-C.
We are converting a legacy application to use AWS instead of a home-grown server. That application does arbitrary HTTP requests, which means that we are going to want to sign HTTP requests. We have been trying to build this based on the documentation, along with some help from the implementation here.
We are doing this against Cognito User Pools. So we set up the pool:
Along with using
-getDetails
to log the user in with no code that's particularly interesting.After the user is logged in, we make sure to grab the credentials and cache them locally:
We are getting back credentials from that call (digits changed to invalidate):
Once we have the credentials cached, we are using them to sign the HTTP request using fancy math:
And then we implement the delegate methods for NSURLSession in uninteresting ways (they simply log what they get called with).
Running this with a valid user that is logged in and can access the file in S3 using the transfer manager (so we know for a fact the user can download the file), we end up getting the following in the logs:
This seems to imply that somewhere we are not hooking something up correctly. However, I cannot find any other place where we could conceivably get that user's Access Key and Secret Key, other than the
credentials
on thecredentialsProvider
; that would seem to be the most logical place for them.So: