Skip to content

Commit

Permalink
Add Interecptors, ConnectionProviderFactory, and integration test tar…
Browse files Browse the repository at this point in the history
…get (#4)

* - Moved over the three common interceptor classes: APIKey, IAM, OIDC.
  Refactored the initialization methods to allow consumers to use without taking on a dependency on the auth provider classes vended by AppSync client.
  The reason for this is to prevent a breaking change, we cannot move the auth provider classes over to AppSyncRealTimeClient or AppSyncClient consumers will
  have to take on AppSyncRealTimeClient as a dependency.
- Add ConnectionProviderFactory class for creating connection providers with auth interceptors.
  This is the entry point for consumers to pass (url, authInterceptor) to get a connection provider back.
- Add integration test target with AppSync backend provisioned

* Moved out IAMAuthInterceptor to remove dependency on AWSCore

* removed AWSCore from podspec
  • Loading branch information
lawmicha authored Mar 10, 2020
1 parent dc0bc0a commit af4767b
Show file tree
Hide file tree
Showing 31 changed files with 1,500 additions and 14 deletions.
5 changes: 2 additions & 3 deletions AppSyncRealTimeClient.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|

s.name = 'AppSyncRealTimeClient'
s.version = '1.0.1'
s.version = '1.0.2'
s.summary = 'Amazon Web Services AppSync RealTime Client for iOS.'

s.description = 'AppSync RealTime Client provides subscription connections to AppSync websocket endpoints'
Expand All @@ -17,5 +17,4 @@ Pod::Spec.new do |s|

s.source_files = 'AppSyncRealTimeClient/**/*.swift'
s.dependency 'Starscream', '~> 3.0.2'

end
end
452 changes: 449 additions & 3 deletions AppSyncRealTimeClient.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "21D38B3D2409AFBD00EC2A8D"
BuildableName = "AppSyncRealTimeClientIntegrationTests.xctest"
BlueprintName = "AppSyncRealTimeClientIntegrationTests"
ReferencedContainer = "container:AppSyncRealTimeClient.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "21D38B522409B93F00EC2A8D"
BuildableName = "HostApp.app"
BlueprintName = "HostApp"
ReferencedContainer = "container:AppSyncRealTimeClient.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "21D38B3D2409AFBD00EC2A8D"
BuildableName = "AppSyncRealTimeClientIntegrationTests.xctest"
BlueprintName = "AppSyncRealTimeClientIntegrationTests"
ReferencedContainer = "container:AppSyncRealTimeClient.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "21D38B522409B93F00EC2A8D"
BuildableName = "HostApp.app"
BlueprintName = "HostApp"
ReferencedContainer = "container:AppSyncRealTimeClient.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "21D38B522409B93F00EC2A8D"
BuildableName = "HostApp.app"
BlueprintName = "HostApp"
ReferencedContainer = "container:AppSyncRealTimeClient.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Create connection providers to connect to the websocket endpoint of the AppSync endpoint.
public struct ConnectionProviderFactory {

public static func createConnectionProvider(for url: URL,
authInterceptor: AuthInterceptor,
connectionType: SubscriptionConnectionType) -> ConnectionProvider {
let provider = ConnectionProviderFactory.createConnectionProvider(for: url, connectionType: connectionType)

if let messageInterceptable = provider as? MessageInterceptable {
messageInterceptable.addInterceptor(authInterceptor)
}
if let connectionInterceptable = provider as? ConnectionInterceptable {
connectionInterceptable.addInterceptor(RealtimeGatewayURLInterceptor())
connectionInterceptable.addInterceptor(authInterceptor)
}

return provider
}

static func createConnectionProvider(for url: URL, connectionType: SubscriptionConnectionType) -> ConnectionProvider {
switch connectionType {
case .appSyncRealtime:
let websocketProvider = StarscreamAdapter()
let connectionProvider = RealtimeConnectionProvider(for: url, websocket: websocketProvider)
return connectionProvider
}
}
}
99 changes: 99 additions & 0 deletions AppSyncRealTimeClient/Interceptor/APIKeyAuthInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Auth interceptor for API Key based authentication
public class APIKeyAuthInterceptor: AuthInterceptor {

let apiKey: String

public init(_ apiKey: String) {
self.apiKey = apiKey
}

/// Intercept the connection and adds header, payload query to the request url.
///
/// The value of header should be the base64 string of the following:
/// * "host": <string> : this is the host for the AppSync endpoint
/// * "x-amz-date": <string> : UTC timestamp in the following ISO 8601 format: YYYYMMDD'T'HHMMSS'Z'
/// * "x-api-key": <string> : Api key configured for AppSync API
/// The value of payload is {}
/// - Parameter request: Signed request
public func interceptConnection(_ request: AppSyncConnectionRequest,
for endpoint: URL) -> AppSyncConnectionRequest {
let host = endpoint.host!
let authHeader = APIKeyAuthenticationHeader(apiKey: apiKey, host: host)
let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader)

let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8)
let payloadBase64 = payloadData?.base64EncodedString()

guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else {
return request
}
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
}
let signedRequest = AppSyncConnectionRequest(url: url)
return signedRequest
}

public func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage {
let host = endpoint.host!
switch message.messageType {
case .subscribe:
let authHeader = APIKeyAuthenticationHeader(apiKey: apiKey, 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:
AppSyncLogger.debug("Message type does not need signing - \(message.messageType)")
}
return message
}
}

/// Authentication header for API key based auth
private class APIKeyAuthenticationHeader: AuthenticationHeader {
static let ISO8601DateFormat: String = "yyyyMMdd'T'HHmmss'Z'"
let date: String?
let apiKey: String

var formatter: DateFormatter = {
var formatter = DateFormatter()
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = ISO8601DateFormat
return formatter
}()

init(apiKey: String, host: String) {
self.date = formatter.string(from: Date())
self.apiKey = apiKey
super.init(host: host)
}

private enum CodingKeys: String, CodingKey {
case date = "x-amz-date"
case apiKey = "x-api-key"
}

override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(date, forKey: .date)
try container.encode(apiKey, forKey: .apiKey)
try super.encode(to: encoder)
}
}
91 changes: 91 additions & 0 deletions AppSyncRealTimeClient/Interceptor/OIDCAuthInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

public class OIDCAuthInterceptor: AuthInterceptor {

let authProvider: OIDCAuthProvider

public init (_ authProvider: OIDCAuthProvider) {
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:
AppSyncLogger.debug("Message type does not need signing - \(message.messageType)")
}
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:
return request
}

let authHeader = UserPoolsAuthenticationHeader(token: jwtToken, host: host)
let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader)

let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8)
let payloadBase64 = payloadData?.base64EncodedString()

guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else {
return request
}
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
}
let signedRequest = AppSyncConnectionRequest(url: url)
return signedRequest
}
}

/// Authentication header for user pool based auth
private class UserPoolsAuthenticationHeader: AuthenticationHeader {
let authorization: String

init(token: String, host: String) {
self.authorization = token
super.init(host: host)
}

private enum CodingKeys: String, CodingKey {
case authorization = "Authorization"
}

override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(authorization, forKey: .authorization)
try super.encode(to: encoder)
}
}
Loading

0 comments on commit af4767b

Please sign in to comment.