Skip to content

Commit 45500e1

Browse files
authored
fix(API): Subscriptions with IAM match signed headers (#1139)
* fix(API): Subscriptions with IAM match signed headers * fix(API): add comments
1 parent 4a93798 commit 45500e1

File tree

7 files changed

+526
-107
lines changed

7 files changed

+526
-107
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
212CE70E23E9E991007D8E71 /* PaginationDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE70923E9E991007D8E71 /* PaginationDecorator.swift */; };
4848
212CE70F23E9E991007D8E71 /* ModelDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE70A23E9E991007D8E71 /* ModelDecorator.swift */; };
4949
212CE71323E9F2ED007D8E71 /* DirectiveNameDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212CE71223E9F2ED007D8E71 /* DirectiveNameDecorator.swift */; };
50+
2136FAE72620366700A95F6F /* MockAWSSignatureV4Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2136FAE62620366700A95F6F /* MockAWSSignatureV4Signer.swift */; };
5051
21409C552384C55D000A53C9 /* LabelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C542384C55D000A53C9 /* LabelType.swift */; };
5152
21409C5A2384C57D000A53C9 /* GraphQLMutationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C572384C57D000A53C9 /* GraphQLMutationType.swift */; };
5253
21409C5B2384C57D000A53C9 /* GraphQLQueryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C582384C57D000A53C9 /* GraphQLQueryType.swift */; };
@@ -856,6 +857,7 @@
856857
212CE71C23EA1847007D8E71 /* GraphQLSyncQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSyncQueryTests.swift; sourceTree = "<group>"; };
857858
212CE72023EA184F007D8E71 /* GraphQLSubscriptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSubscriptionTests.swift; sourceTree = "<group>"; };
858859
213481D8242AFA62001966DE /* AnyModelTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyModelTester.swift; sourceTree = "<group>"; };
860+
2136FAE62620366700A95F6F /* MockAWSSignatureV4Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAWSSignatureV4Signer.swift; sourceTree = "<group>"; };
859861
21409C4C23847E41000A53C9 /* LabelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelType.swift; sourceTree = "<group>"; };
860862
21409C542384C55D000A53C9 /* LabelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelType.swift; sourceTree = "<group>"; };
861863
21409C572384C57D000A53C9 /* GraphQLMutationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLMutationType.swift; sourceTree = "<group>"; };
@@ -3147,6 +3149,7 @@
31473149
FA131AE023610B6A0008381C /* AWSPluginsTestCommon.h */,
31483150
FA131AE123610B6A0008381C /* Info.plist */,
31493151
2125E2A72321DCDD00B3DEB5 /* MockAWSAuthService.swift */,
3152+
2136FAE62620366700A95F6F /* MockAWSSignatureV4Signer.swift */,
31503153
);
31513154
path = AWSPluginsTestCommon;
31523155
sourceTree = "<group>";
@@ -4738,6 +4741,7 @@
47384741
buildActionMask = 2147483647;
47394742
files = (
47404743
FAB2C9A52384B21C008EE879 /* MockAWSAuthService.swift in Sources */,
4744+
2136FAE72620366700A95F6F /* MockAWSSignatureV4Signer.swift in Sources */,
47414745
);
47424746
runOnlyForDeploymentPostprocessing = 0;
47434747
};

AmplifyPlugins/API/APICategoryPlugin.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
212B29212592454400593ED5 /* AppSyncListPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B29202592454400593ED5 /* AppSyncListPayload.swift */; };
2121
212B298B2592519800593ED5 /* GraphQLConnectionScenario3Tests+List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B298A2592519800593ED5 /* GraphQLConnectionScenario3Tests+List.swift */; };
2222
212B299F259251F100593ED5 /* GraphQLModelBasedTests+List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212B299E259251F100593ED5 /* GraphQLModelBasedTests+List.swift */; };
23+
2136FAAE261FF96600A95F6F /* GraphQLWithIAMIntegrationTests-credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 2136FAAC261FF96400A95F6F /* GraphQLWithIAMIntegrationTests-credentials.json */; };
24+
2136FAAF261FF96600A95F6F /* GraphQLWithIAMIntegrationTests-amplifyconfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = 2136FAAD261FF96500A95F6F /* GraphQLWithIAMIntegrationTests-amplifyconfiguration.json */; };
25+
2136FABB262022D700A95F6F /* IAMAuthInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2136FABA262022D700A95F6F /* IAMAuthInterceptorTests.swift */; };
2326
21409C4F2384BA7E000A53C9 /* APIOperationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C4E2384BA7E000A53C9 /* APIOperationResponse.swift */; };
2427
21409C602384DF17000A53C9 /* RESTOperationRequest+RESTRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C5F2384DF17000A53C9 /* RESTOperationRequest+RESTRequest.swift */; };
2528
21409C7223850BEE000A53C9 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21409C7123850BEE000A53C9 /* Todo.swift */; };
@@ -315,6 +318,9 @@
315318
212B29202592454400593ED5 /* AppSyncListPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncListPayload.swift; sourceTree = "<group>"; };
316319
212B298A2592519800593ED5 /* GraphQLConnectionScenario3Tests+List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphQLConnectionScenario3Tests+List.swift"; sourceTree = "<group>"; };
317320
212B299E259251F100593ED5 /* GraphQLModelBasedTests+List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphQLModelBasedTests+List.swift"; sourceTree = "<group>"; };
321+
2136FAAC261FF96400A95F6F /* GraphQLWithIAMIntegrationTests-credentials.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "GraphQLWithIAMIntegrationTests-credentials.json"; sourceTree = "<group>"; };
322+
2136FAAD261FF96500A95F6F /* GraphQLWithIAMIntegrationTests-amplifyconfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "GraphQLWithIAMIntegrationTests-amplifyconfiguration.json"; sourceTree = "<group>"; };
323+
2136FABA262022D700A95F6F /* IAMAuthInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAMAuthInterceptorTests.swift; sourceTree = "<group>"; };
318324
21409C4E2384BA7E000A53C9 /* APIOperationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIOperationResponse.swift; sourceTree = "<group>"; };
319325
21409C5F2384DF17000A53C9 /* RESTOperationRequest+RESTRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTOperationRequest+RESTRequest.swift"; sourceTree = "<group>"; };
320326
21409C6723850A9E000A53C9 /* AWSAPICategoryPluginTestCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AWSAPICategoryPluginTestCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -650,6 +656,14 @@
650656
path = AuthDirective;
651657
sourceTree = "<group>";
652658
};
659+
2136FAB9262022C000A95F6F /* SubscriptionInterceptor */ = {
660+
isa = PBXGroup;
661+
children = (
662+
2136FABA262022D700A95F6F /* IAMAuthInterceptorTests.swift */,
663+
);
664+
path = SubscriptionInterceptor;
665+
sourceTree = "<group>";
666+
};
653667
21409C612384F7FD000A53C9 /* Models */ = {
654668
isa = PBXGroup;
655669
children = (
@@ -772,6 +786,8 @@
772786
217856612380C60400A30D19 /* GraphQLWithIAMIntegrationTests */ = {
773787
isa = PBXGroup;
774788
children = (
789+
2136FAAD261FF96500A95F6F /* GraphQLWithIAMIntegrationTests-amplifyconfiguration.json */,
790+
2136FAAC261FF96400A95F6F /* GraphQLWithIAMIntegrationTests-credentials.json */,
775791
217856622380C60400A30D19 /* GraphQLWithIAMIntegrationTests.swift */,
776792
217856642380C60400A30D19 /* Info.plist */,
777793
21598CE823A0037800529F29 /* README.md */,
@@ -1186,6 +1202,7 @@
11861202
children = (
11871203
B4DFA5DB237A611D0013E17B /* APIKeyURLRequestInterceptorTests.swift */,
11881204
B4DFA5DC237A611D0013E17B /* IAMURLRequestInterceptorTests.swift */,
1205+
2136FAB9262022C000A95F6F /* SubscriptionInterceptor */,
11891206
B4DFA5DA237A611D0013E17B /* UserPoolRequestInterceptorTests.swift */,
11901207
);
11911208
path = Interceptor;
@@ -1611,10 +1628,12 @@
16111628
219A88A223EE034F00BBC5F2 /* RESTWithUserPoolIntegrationTests-amplifyconfiguration.json in Resources */,
16121629
21598CE3239FF54E00529F29 /* RESTWithIAMIntegrationTests-amplifyconfiguration.json in Resources */,
16131630
21233D0B2469EBA000039337 /* GraphQLAuthDirectiveIntegrationTests-amplifyconfiguration.json in Resources */,
1631+
2136FAAF261FF96600A95F6F /* GraphQLWithIAMIntegrationTests-amplifyconfiguration.json in Resources */,
16141632
B478F6E32374E0CF00C4F92B /* Main.storyboard in Resources */,
16151633
B478F6E12374E0CF00C4F92B /* Assets.xcassets in Resources */,
16161634
21598CF223A0164A00529F29 /* GraphQLWithUserPoolIntegrationTests-amplifyconfiguration.json in Resources */,
16171635
21F40A2B23A0423C0074678E /* GraphQLSyncBasedTests-amplifyconfiguration.json in Resources */,
1636+
2136FAAE261FF96600A95F6F /* GraphQLWithIAMIntegrationTests-credentials.json in Resources */,
16181637
21F40A2E23A0707E0074678E /* GraphQLModelBasedTests-amplifyconfiguration.json in Resources */,
16191638
219A88A523EE054000BBC5F2 /* RESTWithUserPoolIntegrationTests-credentials.json in Resources */,
16201639
21A3EFC623A946590095D8E6 /* GraphQLWithUserPoolIntegrationTests-credentials.json in Resources */,
@@ -2374,6 +2393,7 @@
23742393
isa = PBXSourcesBuildPhase;
23752394
buildActionMask = 2147483647;
23762395
files = (
2396+
2136FABB262022D700A95F6F /* IAMAuthInterceptorTests.swift in Sources */,
23772397
B4DFA5EE237A611D0013E17B /* RESTRequestUtilsTests.swift in Sources */,
23782398
B4DFA5F0237A611D0013E17B /* RESTRequestUtils+ValidatorTests.swift in Sources */,
23792399
B4DFA5F2237A611D0013E17B /* RESTOperationRequestValidateTests.swift in Sources */,

AmplifyPlugins/API/AWSAPICategoryPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import AppSyncRealTimeClient
1313

1414
class IAMAuthInterceptor: AuthInterceptor {
1515

16+
private static let defaultLowercasedHeaderKeys: Set = [SubscriptionConstants.authorizationkey.lowercased(),
17+
RealtimeProviderConstants.acceptKey.lowercased(),
18+
RealtimeProviderConstants.contentEncodingKey.lowercased(),
19+
RealtimeProviderConstants.contentTypeKey.lowercased(),
20+
RealtimeProviderConstants.amzDate.lowercased(),
21+
RealtimeProviderConstants.iamSecurityTokenKey.lowercased()]
22+
1623
let authProvider: AWSCredentialsProvider
1724
let region: AWSRegionType
1825

@@ -77,12 +84,34 @@ class IAMAuthInterceptor: AuthInterceptor {
7784
}
7885
let signer: AWSSignatureV4Signer = AWSSignatureV4Signer(credentialsProvider: authProvider,
7986
endpoint: awsEndpoint)
80-
let semaphore = DispatchSemaphore(value: 0)
8187
let mutableRequest = NSMutableURLRequest(url: endpoint)
88+
return getAuthHeader(host: host,
89+
mutableRequest: mutableRequest,
90+
signer: signer,
91+
amzDate: date,
92+
payload: payload)
93+
}
94+
95+
/// The process of getting the auth header for an IAM based authencation request is as follows:
96+
///
97+
/// 1. A request is created with the IAM based auth headers (date, accept, content encoding, content type, and
98+
/// additional headers from the `mutableRequest`.
99+
///
100+
/// 2. The request is SigV4 signed by using all the available headers on the request. By signing the request, the signature is added to
101+
/// the request headers as authorization and security token.
102+
///
103+
/// 3. The signed request headers are stored in an `IAMAuthenticationHeader` object, used for further encoding to
104+
/// be added to the request for establishing the subscription connection.
105+
func getAuthHeader(host: String,
106+
mutableRequest: NSMutableURLRequest,
107+
signer: AWSSignatureV4Signer,
108+
amzDate: String,
109+
payload: String) -> IAMAuthenticationHeader {
110+
let semaphore = DispatchSemaphore(value: 0)
82111
mutableRequest.httpMethod = "POST"
83112
mutableRequest.addValue(RealtimeProviderConstants.iamAccept,
84113
forHTTPHeaderField: RealtimeProviderConstants.acceptKey)
85-
mutableRequest.addValue(date, forHTTPHeaderField: RealtimeProviderConstants.amzDate)
114+
mutableRequest.addValue(amzDate, forHTTPHeaderField: RealtimeProviderConstants.amzDate)
86115
mutableRequest.addValue(RealtimeProviderConstants.iamEncoding,
87116
forHTTPHeaderField: RealtimeProviderConstants.contentEncodingKey)
88117
mutableRequest.addValue(RealtimeProviderConstants.iamConentType,
@@ -94,61 +123,97 @@ class IAMAuthInterceptor: AuthInterceptor {
94123
return nil
95124
}
96125
semaphore.wait()
126+
97127
let authorization = mutableRequest.allHTTPHeaderFields?[SubscriptionConstants.authorizationkey] ?? ""
98128
let securityToken = mutableRequest.allHTTPHeaderFields?[RealtimeProviderConstants.iamSecurityTokenKey] ?? ""
99-
let authHeader = IAMAuthenticationHeader(authorization: authorization,
100-
host: host,
101-
token: securityToken,
102-
date: date,
103-
accept: RealtimeProviderConstants.iamAccept,
104-
contentEncoding: RealtimeProviderConstants.iamEncoding,
105-
contentType: RealtimeProviderConstants.iamConentType)
106-
return authHeader
129+
let additionalHeaders = mutableRequest.allHTTPHeaderFields?.filter {
130+
!Self.defaultLowercasedHeaderKeys.contains($0.key.lowercased())
131+
}
132+
133+
return IAMAuthenticationHeader(host: host,
134+
authorization: authorization,
135+
securityToken: securityToken,
136+
amzDate: amzDate,
137+
accept: RealtimeProviderConstants.iamAccept,
138+
contentEncoding: RealtimeProviderConstants.iamEncoding,
139+
contentType: RealtimeProviderConstants.iamConentType,
140+
additionalHeaders: additionalHeaders)
107141
}
108142
}
109143

110-
/// Authentication header for IAM based auth
111-
private class IAMAuthenticationHeader: AuthenticationHeader {
144+
/// Stores the headers for an IAM based authentication. This object can be serialized to a JSON object and passed as the
145+
/// headers value for establishing subscription connections. This is used as part of the overall interceptor logic
146+
/// which expects a subclass of `AuthenticationHeader` to be returned.
147+
/// See `IAMAuthInterceptor.getAuthHeader` for more details.
148+
class IAMAuthenticationHeader: AuthenticationHeader {
112149
let authorization: String
113150
let securityToken: String
114-
let date: String
151+
let amzDate: String
115152
let accept: String
116153
let contentEncoding: String
117154
let contentType: String
118155

119-
init(authorization: String,
120-
host: String,
121-
token: String,
122-
date: String,
156+
/// Additional headers that are not one of the expected headers in the request, but because additional headers are
157+
/// also signed (and added the authorization header), they are required to be stored here to be further encoded.
158+
let additionalHeaders: [String: String]?
159+
160+
init(host: String,
161+
authorization: String,
162+
securityToken: String,
163+
amzDate: String,
123164
accept: String,
124165
contentEncoding: String,
125-
contentType: String) {
126-
self.date = date
166+
contentType: String,
167+
additionalHeaders: [String: String]?) {
127168
self.authorization = authorization
128-
self.securityToken = token
169+
self.securityToken = securityToken
170+
self.amzDate = amzDate
129171
self.accept = accept
130172
self.contentEncoding = contentEncoding
131173
self.contentType = contentType
174+
self.additionalHeaders = additionalHeaders
132175
super.init(host: host)
133176
}
134177

135-
private enum CodingKeys: String, CodingKey {
136-
case authorization = "Authorization"
137-
case accept
138-
case contentEncoding = "content-encoding"
139-
case contentType = "content-type"
140-
case date = "x-amz-date"
141-
case securityToken = "x-amz-security-token"
178+
private struct DynamicCodingKeys: CodingKey {
179+
var stringValue: String
180+
init?(stringValue: String) {
181+
self.stringValue = stringValue
182+
}
183+
var intValue: Int?
184+
init?(intValue: Int) {
185+
// We are not using this, thus just return nil. If we don't return nil, then it is expected all of the
186+
// stored properties are initialized, forcing the implementation to have logic that maintains the two
187+
// properties `stringValue` and `intValue`. Since we don't have a string representation of an int value
188+
// and aren't using int values for determining the coding key, then simply return nil since the encoder
189+
// will always pass in the header key string.
190+
self.intValue = intValue
191+
self.stringValue = ""
192+
193+
}
142194
}
143195

144196
override func encode(to encoder: Encoder) throws {
145-
var container = encoder.container(keyedBy: CodingKeys.self)
146-
try container.encode(authorization, forKey: .authorization)
147-
try container.encode(accept, forKey: .accept)
148-
try container.encode(contentEncoding, forKey: .contentEncoding)
149-
try container.encode(contentType, forKey: .contentType)
150-
try container.encode(date, forKey: .date)
151-
try container.encode(securityToken, forKey: .securityToken)
197+
var container = encoder.container(keyedBy: DynamicCodingKeys.self)
198+
// Force unwrapping when creating a `DynamicCodingKeys` will always be successful since the string constructor
199+
// will never return nil even though the constructor is optional (conformance to CodingKey).
200+
try container.encode(authorization,
201+
forKey: DynamicCodingKeys(stringValue: SubscriptionConstants.authorizationkey)!)
202+
try container.encode(securityToken,
203+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.iamSecurityTokenKey)!)
204+
try container.encode(amzDate,
205+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.amzDate)!)
206+
try container.encode(accept,
207+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.acceptKey)!)
208+
try container.encode(contentEncoding,
209+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentEncodingKey)!)
210+
try container.encode(contentType,
211+
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentTypeKey)!)
212+
if let headers = additionalHeaders {
213+
for (key, value) in headers {
214+
try container.encode(value, forKey: DynamicCodingKeys(stringValue: key)!)
215+
}
216+
}
152217
try super.encode(to: encoder)
153218
}
154219
}

0 commit comments

Comments
 (0)