|
| 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