diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+ConnectionInterceptable.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+ConnectionInterceptable.swift index a8902373..08a47e03 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+ConnectionInterceptable.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+ConnectionInterceptable.swift @@ -15,9 +15,41 @@ extension RealtimeConnectionProvider: ConnectionInterceptable { public func interceptConnection( _ request: AppSyncConnectionRequest, - for endpoint: URL - ) -> AppSyncConnectionRequest { - let finalRequest = connectionInterceptors.reduce(request) { $1.interceptConnection($0, for: endpoint) } - return finalRequest + for endpoint: URL, + completion: (AppSyncConnectionRequest) -> Void) { + + chainInterceptors( + index: 0, + request: request, + endpoint: endpoint, + completion: completion) + } + + private func chainInterceptors( + index: Int, + request: AppSyncConnectionRequest, + endpoint: URL, + completion: (AppSyncConnectionRequest) -> Void) { + + guard index < connectionInterceptors.count else { + completion(request) + return + } + let interceptor = connectionInterceptors[index] + interceptor.interceptConnection(request, for: endpoint) { interceptedRequest in + chainInterceptors( + index: index + 1, + request: interceptedRequest, + endpoint: endpoint, + completion: completion) + } + } + + // MARK: Deprecated method + + public func interceptConnection( + _ request: AppSyncConnectionRequest, + for endpoint: URL) -> AppSyncConnectionRequest { + fatalError("Should not be invoked, use the callback based api") } } diff --git a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+MessageInterceptable.swift b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+MessageInterceptable.swift index 5f8b529b..926ef014 100644 --- a/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+MessageInterceptable.swift +++ b/AppSyncRealTimeClient/ConnectionProvider/AppsyncRealtimeConnection/RealtimeConnectionProvider+MessageInterceptable.swift @@ -13,8 +13,41 @@ extension RealtimeConnectionProvider: MessageInterceptable { messageInterceptors.append(interceptor) } + public func interceptMessage( + _ message: AppSyncMessage, + for endpoint: URL, + completion: (AppSyncMessage) -> Void) { + + chainInterceptors( + index: 0, + message: message, + endpoint: endpoint, + completion: completion) + } + + private func chainInterceptors( + index: Int, + message: AppSyncMessage, + endpoint: URL, + completion: (AppSyncMessage) -> Void) { + + guard index < messageInterceptors.count else { + completion(message) + return + } + let interceptor = messageInterceptors[index] + interceptor.interceptMessage(message, for: endpoint) { interceptedMessage in + chainInterceptors( + index: index + 1, + message: interceptedMessage, + endpoint: endpoint, + completion: completion) + } + } + + // MARK: Deprecated method + public func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage { - let finalMessage = messageInterceptors.reduce(message) { $1.interceptMessage($0, for: endpoint) } - return finalMessage + fatalError("Should not be invoked, use the callback based api") } } diff --git a/AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift b/AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift index da4cbea9..2147d505 100644 --- a/AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift +++ b/AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift @@ -15,70 +15,102 @@ public class OIDCAuthInterceptor: AuthInterceptor { self.authProvider = authProvider } - public func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage { - let host = endpoint.host! - let jwtToken: String - switch authProvider.getLatestAuthToken() { - case .success(let token): - jwtToken = token - case .failure: - return message - } - switch message.messageType { - case .subscribe: - let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host) - var payload = message.payload ?? AppSyncMessage.Payload() - payload.authHeader = authHeader - - let signedMessage = AppSyncMessage( - id: message.id, - payload: payload, - type: message.messageType - ) - return signedMessage - default: - break + public func interceptMessage( + _ message: AppSyncMessage, + for endpoint: URL, + completion: (AppSyncMessage) -> Void) { + guard let host = endpoint.host else { + completion(message) + return + } + + authProvider.getLatestAuthToken { result in + let jwtToken: String + switch result { + case .success(let token): + jwtToken = token + case .failure: + completion(message) + return + } + guard case .subscribe = message.messageType else { + completion(message) + return + } + let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host) + var payload = message.payload ?? AppSyncMessage.Payload() + payload.authHeader = authHeader + + let signedMessage = AppSyncMessage( + id: message.id, + payload: payload, + type: message.messageType + ) + completion(signedMessage) + return + } } - return message - } public func interceptConnection( _ request: AppSyncConnectionRequest, - for endpoint: URL - ) -> AppSyncConnectionRequest { - let host = endpoint.host! - let jwtToken: String - switch authProvider.getLatestAuthToken() { - case .success(let token): - jwtToken = token - case .failure: - // A user that is not signed in should receive an unauthorized error from the connection attempt. This code - // achieves this by always creating a valid request to AppSync even when the token cannot be retrieved. The - // request sent to AppSync will receive a response indicating the request is unauthorized. If we do not use - // empty token string and perform the remaining logic of the request construction then it will fail request - // validation at AppSync before the authorization check, which ends up being propagated back to the caller - // as a "bad request". Example of bad requests are when the header and payload query strings are missing - // or when the data is not base64 encoded. - jwtToken = "" - } + for endpoint: URL, + completion: (AppSyncConnectionRequest) -> Void) { + + guard let host = endpoint.host else { + completion(request) + return + } - let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host) - let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader) + authProvider.getLatestAuthToken { result in + let jwtToken: String + switch result { + case .success(let token): + jwtToken = token + case .failure: + // A user that is not signed in should receive an unauthorized error from the connection attempt. This code + // achieves this by always creating a valid request to AppSync even when the token cannot be retrieved. The + // request sent to AppSync will receive a response indicating the request is unauthorized. If we do not use + // empty token string and perform the remaining logic of the request construction then it will fail request + // validation at AppSync before the authorization check, which ends up being propagated back to the caller + // as a "bad request". Example of bad requests are when the header and payload query strings are missing + // or when the data is not base64 encoded. + jwtToken = "" + } - let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8) - let payloadBase64 = payloadData?.base64EncodedString() + let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host) + let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader) - guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else { - return request + let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8) + let payloadBase64 = payloadData?.base64EncodedString() + + guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else { + completion(request) + return + } + let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth) + let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64) + urlComponents.queryItems = [headerQuery, payloadQuery] + guard let url = urlComponents.url else { + completion(request) + return + } + let signedRequest = AppSyncConnectionRequest(url: url) + completion(signedRequest) + return + } } - let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth) - let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64) - urlComponents.queryItems = [headerQuery, payloadQuery] - guard let url = urlComponents.url else { - return request + + public func interceptMessage( + _ message: AppSyncMessage, + for endpoint: URL) -> AppSyncMessage { + fatalError("") } - let signedRequest = AppSyncConnectionRequest(url: url) - return signedRequest + + public func interceptConnection( + _ request: AppSyncConnectionRequest, + for endpoint: URL + ) -> AppSyncConnectionRequest { + fatalError("") } } diff --git a/AppSyncRealTimeClient/Support/OIDCAuthProvider.swift b/AppSyncRealTimeClient/Support/OIDCAuthProvider.swift index 9a4f7094..a45e5576 100644 --- a/AppSyncRealTimeClient/Support/OIDCAuthProvider.swift +++ b/AppSyncRealTimeClient/Support/OIDCAuthProvider.swift @@ -7,4 +7,14 @@ public protocol OIDCAuthProvider { func getLatestAuthToken() -> Result + + func getLatestAuthToken(completion: (Result) -> Void ) +} + +public extension OIDCAuthProvider { + + func getLatestAuthToken(completion: (Result) -> Void) { + let result = getLatestAuthToken() + completion(result) + } }