Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Async IAM signing #2871

Merged
merged 4 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
B478F6E12374E0CF00C4F92B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D52374E0CF00C4F92B /* Assets.xcassets */; };
B478F6E22374E0CF00C4F92B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D62374E0CF00C4F92B /* LaunchScreen.storyboard */; };
B478F6E32374E0CF00C4F92B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B478F6D82374E0CF00C4F92B /* Main.storyboard */; };
B48D04AF29EE203F000A73BD /* HeaderIAMSigningHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */; };
B4DFA5E0237A611D0013E17B /* MockSessionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C0237A611D0013E17B /* MockSessionFactory.swift */; };
B4DFA5E1237A611D0013E17B /* MockURLSessionTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C1237A611D0013E17B /* MockURLSessionTask.swift */; };
B4DFA5E2237A611D0013E17B /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DFA5C2237A611D0013E17B /* MockURLSession.swift */; };
Expand Down Expand Up @@ -524,6 +525,7 @@
B478F6D72374E0CF00C4F92B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
B478F6D92374E0CF00C4F92B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
B478F6DA2374E0CF00C4F92B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderIAMSigningHelper.swift; sourceTree = "<group>"; };
B4DFA5C0237A611D0013E17B /* MockSessionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSessionFactory.swift; sourceTree = "<group>"; };
B4DFA5C1237A611D0013E17B /* MockURLSessionTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionTask.swift; sourceTree = "<group>"; };
B4DFA5C2237A611D0013E17B /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -900,6 +902,7 @@
21D5286624169E74005186BA /* IAMAuthInterceptor.swift */,
763C857026B0651A005164B2 /* AuthenticationTokenAuthInterceptor.swift */,
6BD4620525380EA200906831 /* OIDCAuthProviderWrapper.swift */,
B48D04AE29EE203F000A73BD /* HeaderIAMSigningHelper.swift */,
);
path = SubscriptionInterceptor;
sourceTree = "<group>";
Expand Down Expand Up @@ -2461,6 +2464,7 @@
buildActionMask = 2147483647;
files = (
21D7A102237B54D90057D00D /* URLSessionBehaviorDelegate.swift in Sources */,
B48D04AF29EE203F000A73BD /* HeaderIAMSigningHelper.swift in Sources */,
21D7A0FD237B54D90057D00D /* URLSession+URLSessionBehavior.swift in Sources */,
212B29212592454400593ED5 /* AppSyncListPayload.swift in Sources */,
21A4F35A25A4F2E800E1047D /* AppSyncListResponse.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import AWSCore
import AppSyncRealTimeClient

struct HeaderIAMSigningHelper {

let host: String
let date: String
let payload: String
let awsEndpoint: AWSEndpoint
let region: AWSRegionType
let endpointURL: URL

private static let defaultLowercasedHeaderKeys: Set = [
SubscriptionConstants.authorizationkey.lowercased(),
RealtimeProviderConstants.acceptKey.lowercased(),
RealtimeProviderConstants.contentEncodingKey.lowercased(),
RealtimeProviderConstants.contentTypeKey.lowercased(),
RealtimeProviderConstants.amzDate.lowercased(),
RealtimeProviderConstants.iamSecurityTokenKey.lowercased()]

init?(endpoint: URL,
payload: String,
region: AWSRegionType,
dateString: String? = nil
) {
guard let host = endpoint.host else {
return nil
}
let amzDate = NSDate.aws_clockSkewFixed() as NSDate
guard let date = dateString ?? amzDate.aws_stringValue(AWSDateISO8601DateFormat2) else {
return nil
}
guard let awsEndpoint = AWSEndpoint(
region: region,
serviceName: SubscriptionConstants.appsyncServiceName,
url: endpoint) else {
return nil
}
self.date = date
self.host = host
self.region = region
self.payload = payload
self.endpointURL = endpoint
self.awsEndpoint = awsEndpoint
}

func sign(authProvider: AWSCredentialsProvider,
completion: @escaping (IAMAuthenticationHeader) -> Void) {
let signer: AWSSignatureV4Signer = AWSSignatureV4Signer(
credentialsProvider: authProvider,
endpoint: awsEndpoint)
let mutableRequest = NSMutableURLRequest(url: endpointURL)
sign(signer: signer, mutableRequest: mutableRequest, completion: completion)
}

/// The process of getting the auth header for an IAM based authencation request is as follows:
///
/// 1. A request is created with the IAM based auth headers (date, accept, content encoding, content type, and
/// additional headers from the `request`.
///
/// 2. The request is SigV4 signed by using all the available headers on the request.
/// By signing the request, the signature is added to
/// the request headers as authorization and security token.
///
/// 3. The signed request headers are stored in an `IAMAuthenticationHeader` object, used for further encoding to
/// be added to the request for establishing the subscription connection.
func sign(
signer: AWSSignatureV4Signer,
mutableRequest: NSMutableURLRequest,
completion: @escaping (IAMAuthenticationHeader) -> Void) {

mutableRequest.httpMethod = "POST"

mutableRequest.addValue(RealtimeProviderConstants.iamAccept,
forHTTPHeaderField: RealtimeProviderConstants.acceptKey)
mutableRequest.addValue(date, forHTTPHeaderField: RealtimeProviderConstants.amzDate)
mutableRequest.addValue(RealtimeProviderConstants.iamEncoding,
forHTTPHeaderField: RealtimeProviderConstants.contentEncodingKey)
mutableRequest.addValue(RealtimeProviderConstants.iamConentType,
forHTTPHeaderField: RealtimeProviderConstants.contentTypeKey)
mutableRequest.httpBody = payload.data(using: .utf8)

signer.interceptRequest(mutableRequest).continueWith { _ in

let authorization = mutableRequest.allHTTPHeaderFields?[SubscriptionConstants.authorizationkey] ?? ""
let securityToken = mutableRequest.allHTTPHeaderFields?[RealtimeProviderConstants.iamSecurityTokenKey] ?? ""
let additionalHeaders = mutableRequest.allHTTPHeaderFields?.filter {
!Self.defaultLowercasedHeaderKeys.contains($0.key.lowercased())
}

let header = IAMAuthenticationHeader(
host: host,
authorization: authorization,
securityToken: securityToken,
amzDate: date,
accept: RealtimeProviderConstants.iamAccept,
contentEncoding: RealtimeProviderConstants.iamEncoding,
contentType: RealtimeProviderConstants.iamConentType,
additionalHeaders: additionalHeaders)
completion(header)
return nil
}
}
}

/// Stores the headers for an IAM based authentication. This object can be serialized to a JSON object and passed as the
/// headers value for establishing subscription connections. This is used as part of the overall interceptor logic
/// which expects a subclass of `AuthenticationHeader` to be returned.
/// See `IAMAuthInterceptor.getAuthHeader` for more details.
class IAMAuthenticationHeader: AuthenticationHeader {
let authorization: String
let securityToken: String
let amzDate: String
let accept: String
let contentEncoding: String
let contentType: String

/// Additional headers that are not one of the expected headers in the request, but because additional headers are
/// also signed (and added the authorization header), they are required to be stored here to be further encoded.
let additionalHeaders: [String: String]?

init(host: String,
authorization: String,
securityToken: String,
amzDate: String,
accept: String,
contentEncoding: String,
contentType: String,
additionalHeaders: [String: String]?) {
self.authorization = authorization
self.securityToken = securityToken
self.amzDate = amzDate
self.accept = accept
self.contentEncoding = contentEncoding
self.contentType = contentType
self.additionalHeaders = additionalHeaders
super.init(host: host)
}

private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil. If we don't return nil, then it is expected all of the
// stored properties are initialized, forcing the implementation to have logic that maintains the two
// properties `stringValue` and `intValue`. Since we don't have a string representation of an int value
// and aren't using int values for determining the coding key, then simply return nil since the encoder
// will always pass in the header key string.
self.intValue = intValue
self.stringValue = ""

}
}

override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DynamicCodingKeys.self)
// Force unwrapping when creating a `DynamicCodingKeys` will always be successful since the
// string constructor will never return nil even though the constructor is optional
// (conformance to CodingKey).
try container.encode(
authorization,
forKey: DynamicCodingKeys(stringValue: SubscriptionConstants.authorizationkey)!)
try container.encode(
securityToken,
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.iamSecurityTokenKey)!)
try container.encode(
amzDate,
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.amzDate)!)
try container.encode(
accept,
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.acceptKey)!)
try container.encode(
contentEncoding,
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentEncodingKey)!)
try container.encode(
contentType,
forKey: DynamicCodingKeys(stringValue: RealtimeProviderConstants.contentTypeKey)!)
if let headers = additionalHeaders {
for (key, value) in headers {
try container.encode(value, forKey: DynamicCodingKeys(stringValue: key)!)
}
}
try super.encode(to: encoder)
}
}
Loading