Skip to content

Commit fd4f8a4

Browse files
authored
fix(API): remove encoding step before feeding request to signer (#2666)
1 parent a83cc35 commit fd4f8a4

File tree

8 files changed

+180
-159
lines changed

8 files changed

+180
-159
lines changed

.github/workflows/integ_test_api.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ jobs:
190190
scheme: AWSAPIPluginRESTUserPoolTests
191191

192192
api-rest-iam-test:
193-
if: ${{ false }}
194193
needs: prepare-for-test
195194
runs-on: macos-12
196195
environment: IntegrationTest

AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,52 +26,56 @@ struct IAMURLRequestInterceptor: URLRequestInterceptor {
2626
}
2727

2828
func intercept(_ request: URLRequest) async throws -> URLRequest {
29-
guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
30-
throw APIError.unknown("Could not get mutable request", "")
31-
}
32-
guard let url = mutableRequest.url else {
29+
var request = request
30+
31+
guard let url = request.url else {
3332
throw APIError.unknown("Could not get url from mutable request", "")
3433
}
3534
guard let host = url.host else {
3635
throw APIError.unknown("Could not get host from mutable request", "")
3736
}
3837

39-
mutableRequest.setValue(URLRequestConstants.ContentType.applicationJson, forHTTPHeaderField: URLRequestConstants.Header.contentType)
40-
mutableRequest.setValue(host, forHTTPHeaderField: "host")
41-
mutableRequest.setValue(AWSAPIPluginsCore.baseUserAgent(), forHTTPHeaderField: URLRequestConstants.Header.userAgent)
38+
request.setValue(URLRequestConstants.ContentType.applicationJson, forHTTPHeaderField: URLRequestConstants.Header.contentType)
39+
request.setValue(host, forHTTPHeaderField: "host")
40+
request.setValue(AWSAPIPluginsCore.baseUserAgent(), forHTTPHeaderField: URLRequestConstants.Header.userAgent)
4241

43-
let httpMethod = HttpMethodType(rawValue: mutableRequest.httpMethod.uppercased()) ?? .get
42+
let httpMethod = (request.httpMethod?.uppercased())
43+
.flatMap(HttpMethodType.init(rawValue:)) ?? .get
44+
45+
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
4446

4547
let requestBuilder = SdkHttpRequestBuilder()
4648
.withHost(host)
4749
.withPath(url.path)
50+
.withQueryItems(queryItems)
4851
.withMethod(httpMethod)
4952
.withPort(443)
5053
.withProtocol(.https)
51-
.withHeaders(.init(mutableRequest.allHTTPHeaderFields ?? [:]))
52-
.withBody(.data(mutableRequest.httpBody))
54+
.withHeaders(.init(request.allHTTPHeaderFields ?? [:]))
55+
.withBody(.data(request.httpBody))
5356

5457
let signingName: String
5558
switch endpointType {
5659
case .graphQL:
5760
signingName = URLRequestConstants.appSyncServiceName
5861
case .rest:
5962
signingName = URLRequestConstants.apiGatewayServiceName
60-
6163
}
6264

63-
guard let urlRequest = try await AmplifyAWSSignatureV4Signer().sigV4SignedRequest(requestBuilder: requestBuilder,
64-
credentialsProvider: iamCredentialsProvider.getCredentialsProvider(),
65-
signingName: signingName,
66-
signingRegion: region,
67-
date: Date()) else {
65+
guard let urlRequest = try await AmplifyAWSSignatureV4Signer().sigV4SignedRequest(
66+
requestBuilder: requestBuilder,
67+
credentialsProvider: iamCredentialsProvider.getCredentialsProvider(),
68+
signingName: signingName,
69+
signingRegion: region,
70+
date: Date()
71+
) else {
6872
throw APIError.unknown("Unable to sign request", "")
6973
}
7074

7175
for header in urlRequest.headers.headers {
72-
mutableRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name)
76+
request.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name)
7377
}
7478

75-
return mutableRequest as URLRequest
79+
return request
7680
}
7781
}

AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSRESTOperation.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ final public class AWSRESTOperation: AmplifyOperation<
7777
// Construct URL with path
7878
let url: URL
7979
do {
80-
url = try RESTOperationRequestUtils.constructURL(for: endpointConfig.baseURL,
81-
with: request.path,
82-
with: request.queryParameters)
80+
url = try RESTOperationRequestUtils.constructURL(
81+
for: endpointConfig.baseURL,
82+
withPath: request.path,
83+
withParams: request.queryParameters
84+
)
8385
} catch let error as APIError {
8486
dispatch(result: .failure(error))
8587
finish()

AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Internal/URLComponents+sigv4Encoding.swift

Lines changed: 0 additions & 57 deletions
This file was deleted.

AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/RESTOperationRequestUtils.swift

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import Foundation
99
import Amplify
1010

1111
final class RESTOperationRequestUtils {
12-
private init() {
12+
private init() {}
1313

14-
}
15-
16-
static func constructURL(for baseURL: URL,
17-
with path: String?,
18-
with queryParameters: [String: String]?) throws -> URL {
19-
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
20-
throw APIError.invalidURL("Invalid URL: \(baseURL.absoluteString)",
14+
static func constructURL(
15+
for baseURL: URL,
16+
withPath path: String?,
17+
withParams queryParameters: [String: String]?
18+
) throws -> URL {
19+
guard var components = URLComponents(
20+
url: baseURL,
21+
resolvingAgainstBaseURL: false
22+
) else {
23+
throw APIError.invalidURL(
24+
"Invalid URL: \(baseURL.absoluteString)",
2125
"""
2226
Review your API plugin configuration and ensure \(baseURL.absoluteString) is a valid URL for
2327
the 'Endpoint' field.
@@ -29,7 +33,9 @@ final class RESTOperationRequestUtils {
2933
components.path.append(path)
3034
}
3135

32-
try components.encodeQueryItemsPerSigV4Rules(queryParameters)
36+
if let queryParameters = queryParameters {
37+
components.queryItems = try prepareQueryParamsForSigning(params: queryParameters)
38+
}
3339

3440
guard let url = components.url else {
3541
throw APIError.invalidURL(
@@ -63,4 +69,41 @@ final class RESTOperationRequestUtils {
6369
baseRequest.httpBody = requestPayload
6470
return baseRequest
6571
}
72+
73+
private static let permittedQueryParamCharacters = CharacterSet.alphanumerics
74+
.union(.init(charactersIn: "/_-.~"))
75+
76+
private static func prepareQueryParamsForSigning(params: [String: String]) throws -> [URLQueryItem] {
77+
// remove percent encoding to prepare for request signing
78+
// `removingPercentEncoding` is a no-op if the query isn't encoded
79+
func removePercentEncoding(key: String, value: String) -> (String, String) {
80+
(key.removingPercentEncoding ?? key, value.removingPercentEncoding ?? value)
81+
}
82+
83+
// Disallowed characters are checked for in the Swift SDK. However it effectively silently fails
84+
// there by removing any invalid parameters. We're conducting this check here to inform the call-
85+
// site.
86+
func confirmOnlyPermittedCharactersPresent(key: String, value: String) throws -> (String, String) {
87+
guard value.rangeOfCharacter(from: permittedQueryParamCharacters) != nil,
88+
key.rangeOfCharacter(from: permittedQueryParamCharacters) != nil
89+
else {
90+
throw APIError.invalidURL(
91+
"Invalid query parameter.",
92+
"""
93+
Review your Amplify.API call to make sure you are passing \
94+
valid UTF-8 query parameters in your request.
95+
The value passed was '\(key)=\(value)'
96+
"""
97+
)
98+
}
99+
return (key, value)
100+
}
101+
102+
let queryItems = try params
103+
.map(removePercentEncoding)
104+
.map(confirmOnlyPermittedCharactersPresent)
105+
.map(URLQueryItem.init)
106+
107+
return queryItems
108+
}
66109
}

AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/AWSAPIPluginRESTIAMTests.xcscheme

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,6 @@
3737
BlueprintName = "AWSAPIPluginRESTIAMTests"
3838
ReferencedContainer = "container:APIHostApp.xcodeproj">
3939
</BuildableReference>
40-
<SkippedTests>
41-
<Test
42-
Identifier = "RESTWithIAMIntegrationTests/testGetAPIWithEncodedQueryParamsSuccess()">
43-
</Test>
44-
<Test
45-
Identifier = "RESTWithIAMIntegrationTests/testGetAPIWithQueryParamsSuccess()">
46-
</Test>
47-
</SkippedTests>
4840
</TestableReference>
4941
</Testables>
5042
</TestAction>

AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginRESTIAMTests/RESTWithIAMIntegrationTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ class RESTWithIAMIntegrationTests: XCTestCase {
8787
}
8888

8989
func testGetAPIWithEncodedQueryParamsSuccess() async throws {
90-
throw XCTSkip("This test is currently failing with 403 and needs to be fixed")
9190
let request = RESTRequest(path: "/items",
9291
queryParameters: [
9392
"user": "hello%40email.com",
@@ -99,7 +98,6 @@ class RESTWithIAMIntegrationTests: XCTestCase {
9998
}
10099

101100
func testPutAPISuccess() async throws {
102-
throw XCTSkip("This test is currently failing with 403 and needs to be fixed")
103101
let request = RESTRequest(path: "/items")
104102
let data = try await Amplify.API.put(request: request)
105103
let result = String(decoding: data, as: UTF8.self)

0 commit comments

Comments
 (0)