Skip to content

Commit 0041986

Browse files
committed
feat(api): Add async protocol method for request interceptors IAM signing (#2871)
1 parent a25527a commit 0041986

File tree

5 files changed

+336
-195
lines changed

5 files changed

+336
-195
lines changed

AmplifyPlugins/API/APICategoryPlugin.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
B478F6E12374E0CF00C4F92B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D52374E0CF00C4F92B /* Assets.xcassets */; };
148148
B478F6E22374E0CF00C4F92B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D62374E0CF00C4F92B /* LaunchScreen.storyboard */; };
149149
B478F6E32374E0CF00C4F92B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D82374E0CF00C4F92B /* Main.storyboard */; };
150+
B48D04AF29EE203F000A73BD /* HeaderIAMSigningHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */; };
150151
B4DFA5E0237A611D0013E17B /* MockSessionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C0237A611D0013E17B /* MockSessionFactory.swift */; };
151152
B4DFA5E1237A611D0013E17B /* MockURLSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C1237A611D0013E17B /* MockURLSessionTask.swift */; };
152153
B4DFA5E2237A611D0013E17B /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C2237A611D0013E17B /* MockURLSession.swift */; };
@@ -524,6 +525,7 @@
524525
B478F6D72374E0CF00C4F92B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
525526
B478F6D92374E0CF00C4F92B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
526527
B478F6DA2374E0CF00C4F92B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
528+
B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderIAMSigningHelper.swift; sourceTree = "<group>"; };
527529
B4DFA5C0237A611D0013E17B /* MockSessionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSessionFactory.swift; sourceTree = "<group>"; };
528530
B4DFA5C1237A611D0013E17B /* MockURLSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionTask.swift; sourceTree = "<group>"; };
529531
B4DFA5C2237A611D0013E17B /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
@@ -900,6 +902,7 @@
900902
21D5286624169E74005186BA /* IAMAuthInterceptor.swift */,
901903
763C857026B0651A005164B2 /* AuthenticationTokenAuthInterceptor.swift */,
902904
6BD4620525380EA200906831 /* OIDCAuthProviderWrapper.swift */,
905+
B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */,
903906
);
904907
path = SubscriptionInterceptor;
905908
sourceTree = "<group>";
@@ -2461,6 +2464,7 @@
24612464
buildActionMask = 2147483647;
24622465
files = (
24632466
21D7A102237B54D90057D00D /* URLSessionBehaviorDelegate.swift in Sources */,
2467+
B48D04AF29EE203F000A73BD /* HeaderIAMSigningHelper.swift in Sources */,
24642468
21D7A0FD237B54D90057D00D /* URLSession+URLSessionBehavior.swift in Sources */,
24652469
212B29212592454400593ED5 /* AppSyncListPayload.swift in Sources */,
24662470
21A4F35A25A4F2E800E1047D /* AppSyncListResponse.swift in Sources */,
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import AWSCore
10+
import AppSyncRealTimeClient
11+
12+
struct HeaderIAMSigningHelper {
13+
14+
let host: String
15+
let date: String
16+
let payload: String
17+
let awsEndpoint: AWSEndpoint
18+
let region: AWSRegionType
19+
let endpointURL: URL
20+
21+
private static let defaultLowercasedHeaderKeys: Set = [
22+
SubscriptionConstants.authorizationkey.lowercased(),
23+
RealtimeProviderConstants.acceptKey.lowercased(),
24+
RealtimeProviderConstants.contentEncodingKey.lowercased(),
25+
RealtimeProviderConstants.contentTypeKey.lowercased(),
26+
RealtimeProviderConstants.amzDate.lowercased(),
27+
RealtimeProviderConstants.iamSecurityTokenKey.lowercased()]
28+
29+
init?(endpoint: URL,
30+
payload: String,
31+
region: AWSRegionType,
32+
dateString: String? = nil
33+
) {
34+
guard let host = endpoint.host else {
35+
return nil
36+
}
37+
let amzDate = NSDate.aws_clockSkewFixed() as NSDate
38+
guard let date = dateString ?? amzDate.aws_stringValue(AWSDateISO8601DateFormat2) else {
39+
return nil
40+
}
41+
guard let awsEndpoint = AWSEndpoint(
42+
region: region,
43+
serviceName: SubscriptionConstants.appsyncServiceName,
44+
url: endpoint) else {
45+
return nil
46+
}
47+
self.date = date
48+
self.host = host
49+
self.region = region
50+
self.payload = payload
51+
self.endpointURL = endpoint
52+
self.awsEndpoint = awsEndpoint
53+
}
54+
55+
func sign(authProvider: AWSCredentialsProvider,
56+
completion: @escaping (IAMAuthenticationHeader) -> Void) {
57+
let signer: AWSSignatureV4Signer = AWSSignatureV4Signer(
58+
credentialsProvider: authProvider,
59+
endpoint: awsEndpoint)
60+
let mutableRequest = NSMutableURLRequest(url: endpointURL)
61+
sign(signer: signer, mutableRequest: mutableRequest, completion: completion)
62+
}
63+
64+
/// The process of getting the auth header for an IAM based authencation request is as follows:
65+
///
66+
/// 1. A request is created with the IAM based auth headers (date, accept, content encoding, content type, and
67+
/// additional headers from the `request`.
68+
///
69+
/// 2. The request is SigV4 signed by using all the available headers on the request.
70+
/// By signing the request, the signature is added to
71+
/// the request headers as authorization and security token.
72+
///
73+
/// 3. The signed request headers are stored in an `IAMAuthenticationHeader` object, used for further encoding to
74+
/// be added to the request for establishing the subscription connection.
75+
func sign(
76+
signer: AWSSignatureV4Signer,
77+
mutableRequest: NSMutableURLRequest,
78+
completion: @escaping (IAMAuthenticationHeader) -> Void) {
79+
80+
mutableRequest.httpMethod = "POST"
81+
82+
mutableRequest.addValue(RealtimeProviderConstants.iamAccept,
83+
forHTTPHeaderField: RealtimeProviderConstants.acceptKey)
84+
mutableRequest.addValue(date, forHTTPHeaderField: RealtimeProviderConstants.amzDate)
85+
mutableRequest.addValue(RealtimeProviderConstants.iamEncoding,
86+
forHTTPHeaderField: RealtimeProviderConstants.contentEncodingKey)
87+
mutableRequest.addValue(RealtimeProviderConstants.iamConentType,
88+
forHTTPHeaderField: RealtimeProviderConstants.contentTypeKey)
89+
mutableRequest.httpBody = payload.data(using: .utf8)
90+
91+
signer.interceptRequest(mutableRequest).continueWith { _ in
92+
93+
let authorization = mutableRequest.allHTTPHeaderFields?[SubscriptionConstants.authorizationkey] ?? ""
94+
let securityToken = mutableRequest.allHTTPHeaderFields?[RealtimeProviderConstants.iamSecurityTokenKey] ?? ""
95+
let additionalHeaders = mutableRequest.allHTTPHeaderFields?.filter {
96+
!Self.defaultLowercasedHeaderKeys.contains($0.key.lowercased())
97+
}
98+
99+
let header = IAMAuthenticationHeader(
100+
host: host,
101+
authorization: authorization,
102+
securityToken: securityToken,
103+
amzDate: date,
104+
accept: RealtimeProviderConstants.iamAccept,
105+
contentEncoding: RealtimeProviderConstants.iamEncoding,
106+
contentType: RealtimeProviderConstants.iamConentType,
107+
additionalHeaders: additionalHeaders)
108+
completion(header)
109+
return nil
110+
}
111+
}
112+
}
113+
114+
/// Stores the headers for an IAM based authentication. This object can be serialized to a JSON object and passed as the
115+
/// headers value for establishing subscription connections. This is used as part of the overall interceptor logic
116+
/// which expects a subclass of `AuthenticationHeader` to be returned.
117+
/// See `IAMAuthInterceptor.getAuthHeader` for more details.
118+
class IAMAuthenticationHeader: AuthenticationHeader {
119+
let authorization: String
120+
let securityToken: String
121+
let amzDate: String
122+
let accept: String
123+
let contentEncoding: String
124+
let contentType: String
125+
126+
/// Additional headers that are not one of the expected headers in the request, but because additional headers are
127+
/// also signed (and added the authorization header), they are required to be stored here to be further encoded.
128+
let additionalHeaders: [String: String]?
129+
130+
init(host: String,
131+
authorization: String,
132+
securityToken: String,
133+
amzDate: String,
134+
accept: String,
135+
contentEncoding: String,
136+
contentType: String,
137+
additionalHeaders: [String: String]?) {
138+
self.authorization = authorization
139+
self.securityToken = securityToken
140+
self.amzDate = amzDate
141+
self.accept = accept
142+
self.contentEncoding = contentEncoding
143+
self.contentType = contentType
144+
self.additionalHeaders = additionalHeaders
145+
super.init(host: host)
146+
}
147+
148+
private struct DynamicCodingKeys: CodingKey {
149+
var stringValue: String
150+
init?(stringValue: String) {
151+
self.stringValue = stringValue
152+
}
153+
var intValue: Int?
154+
init?(intValue: Int) {
155+
// We are not using this, thus just return nil. If we don't return nil, then it is expected all of the
156+
// stored properties are initialized, forcing the implementation to have logic that maintains the two
157+
// properties `stringValue` and `intValue`. Since we don't have a string representation of an int value
158+
// and aren't using int values for determining the coding key, then simply return nil since the encoder
159+
// will always pass in the header key string.
160+
self.intValue = intValue
161+
self.stringValue = ""
162+
163+
}
164+
}
165+
166+
override func encode(to encoder: Encoder) throws {
167+
var container = encoder.container(keyedBy: DynamicCodingKeys.self)
168+
// Force unwrapping when creating a `DynamicCodingKeys` will always be successful since the
169+
// string constructor will never return nil even though the constructor is optional
170+
// (conformance to CodingKey).
171+
try container.encode(
172+
authorization,
173+
forKey: DynamicCodingKeys(stringValue: SubscriptionConstants.authorizationkey)!)
174+
try container.encode(
175+
securityToken,
176+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.iamSecurityTokenKey)!)
177+
try container.encode(
178+
amzDate,
179+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.amzDate)!)
180+
try container.encode(
181+
accept,
182+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.acceptKey)!)
183+
try container.encode(
184+
contentEncoding,
185+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentEncodingKey)!)
186+
try container.encode(
187+
contentType,
188+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentTypeKey)!)
189+
if let headers = additionalHeaders {
190+
for (key, value) in headers {
191+
try container.encode(value, forKey: DynamicCodingKeys(stringValue: key)!)
192+
}
193+
}
194+
try super.encode(to: encoder)
195+
}
196+
}

0 commit comments

Comments
 (0)