Skip to content

Commit

Permalink
feat: Add AppSync components (#3825)
Browse files Browse the repository at this point in the history
* add Amplify components for AppSync

* Add AWSAppSyncConfigurationTests

* Add signing tests

* remove unnecessary public apis

* add doc comments

* Update API dumps for new version

---------

Co-authored-by: aws-amplify-ops <[email protected]>
  • Loading branch information
lawmicha and aws-amplify-ops authored Aug 23, 2024
1 parent bdfa37a commit 673a075
Show file tree
Hide file tree
Showing 12 changed files with 576 additions and 6 deletions.
3 changes: 2 additions & 1 deletion Amplify/Core/Configuration/AmplifyOutputsData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ public struct AmplifyOutputsData: Codable {
public struct AmplifyOutputs {

/// A closure that resolves the `AmplifyOutputsData` configuration
let resolveConfiguration: () throws -> AmplifyOutputsData
@_spi(InternalAmplifyConfiguration)
public let resolveConfiguration: () throws -> AmplifyOutputsData

/// Resolves configuration with `amplify_outputs.json` in the main bundle.
public static let amplifyOutputs: AmplifyOutputs = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import Amplify // Amplify.Auth
import AWSPluginsCore // AuthAWSCredentialsProvider
import AWSClientRuntime // AWSClientRuntime.CredentialsProviding
import ClientRuntime // SdkHttpRequestBuilder
import AwsCommonRuntimeKit // CommonRuntimeKit.initialize()

extension AWSCognitoAuthPlugin {


/// Creates a AWS IAM SigV4 signer capable of signing AWS AppSync requests.
///
/// **Note**. Although this method is static, **Amplify.Auth** is required to be configured with **AWSCognitoAuthPlugin** as
/// it depends on the credentials provider from Cognito through `Amplify.Auth.fetchAuthSession()`. The static type allows
/// developers to simplify their callsite without having to access the method on the plugin instance.
///
/// - Parameter region: The region of the AWS AppSync API
/// - Returns: A closure that takes in a requestand returns a signed request.
public static func createAppSyncSigner(region: String) -> ((URLRequest) async throws -> URLRequest) {
return { request in
try await signAppSyncRequest(request,
region: region)
}
}

static func signAppSyncRequest(_ urlRequest: URLRequest,
region: Swift.String,
signingName: Swift.String = "appsync",
date: ClientRuntime.Date = Date()) async throws -> URLRequest {
CommonRuntimeKit.initialize()

// Convert URLRequest to SDK's HTTPRequest
guard let requestBuilder = try createAppSyncSdkHttpRequestBuilder(
urlRequest: urlRequest) else {
return urlRequest
}

// Retrieve the credentials from credentials provider
let credentials: AWSClientRuntime.AWSCredentials
let authSession = try await Amplify.Auth.fetchAuthSession()
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {
let awsCredentials = try awsCredentialsProvider.getAWSCredentials().get()
credentials = awsCredentials.toAWSSDKCredentials()
} else {
let error = AuthError.unknown("Auth session does not include AWS credentials information")
throw error
}

// Prepare signing
let flags = SigningFlags(useDoubleURIEncode: true,
shouldNormalizeURIPath: true,
omitSessionToken: false)
let signedBodyHeader: AWSSignedBodyHeader = .none
let signedBodyValue: AWSSignedBodyValue = .empty
let signingConfig = AWSSigningConfig(credentials: credentials,
signedBodyHeader: signedBodyHeader,
signedBodyValue: signedBodyValue,
flags: flags,
date: date,
service: signingName,
region: region,
signatureType: .requestHeaders,
signingAlgorithm: .sigv4)

// Sign request
guard let httpRequest = await AWSSigV4Signer.sigV4SignedRequest(
requestBuilder: requestBuilder,

signingConfig: signingConfig
) else {
return urlRequest
}

// Update original request with new headers
return setHeaders(from: httpRequest, to: urlRequest)
}

static func setHeaders(from sdkRequest: SdkHttpRequest, to urlRequest: URLRequest) -> URLRequest {
var urlRequest = urlRequest
for header in sdkRequest.headers.headers {
urlRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name)
}
return urlRequest
}

static func createAppSyncSdkHttpRequestBuilder(urlRequest: URLRequest) throws -> SdkHttpRequestBuilder? {

guard let url = urlRequest.url,
let host = url.host else {
return nil
}

var headers = urlRequest.allHTTPHeaderFields ?? [:]
headers.updateValue(host, forKey: "host")

let httpMethod = (urlRequest.httpMethod?.uppercased())
.flatMap(HttpMethodType.init(rawValue:)) ?? .get

let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?
.map { ClientRuntime.SDKURLQueryItem(name: $0.name, value: $0.value)} ?? []

let requestBuilder = SdkHttpRequestBuilder()
.withHost(host)
.withPath(url.path)
.withQueryItems(queryItems)
.withMethod(httpMethod)
.withPort(443)
.withProtocol(.https)
.withHeaders(.init(headers))
.withBody(.data(urlRequest.httpBody))

return requestBuilder
}
}

extension AWSPluginsCore.AWSCredentials {

func toAWSSDKCredentials() -> AWSClientRuntime.AWSCredentials {
if let tempCredentials = self as? AWSTemporaryCredentials {
return AWSClientRuntime.AWSCredentials(
accessKey: tempCredentials.accessKeyId,
secret: tempCredentials.secretAccessKey,
expirationTimeout: tempCredentials.expiration,
sessionToken: tempCredentials.sessionToken)
} else {
return AWSClientRuntime.AWSCredentials(
accessKey: accessKeyId,
secret: secretAccessKey,
expirationTimeout: Date())
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import Amplify
@testable import AWSCognitoAuthPlugin

class AWSCognitoAuthPluginAppSyncSignerTests: XCTestCase {

/// Tests translating the URLRequest to the SDKRequest
/// The translation should account for expected fields, as asserted in the test.
func testCreateAppSyncSdkHttpRequestBuilder() throws {
var urlRequest = URLRequest(url: URL(string: "http://graphql.com")!)
urlRequest.httpMethod = "post"
let dataObject = Data()
urlRequest.httpBody = dataObject
guard let sdkRequestBuilder = try AWSCognitoAuthPlugin.createAppSyncSdkHttpRequestBuilder(urlRequest: urlRequest) else {
XCTFail("Could not create SDK request")
return
}

let request = sdkRequestBuilder.build()
XCTAssertEqual(request.host, "graphql.com")
XCTAssertEqual(request.path, "")
XCTAssertEqual(request.queryItems, [])
XCTAssertEqual(request.method, .post)
XCTAssertEqual(request.endpoint.port, 443)
XCTAssertEqual(request.endpoint.protocolType, .https)
XCTAssertEqual(request.endpoint.headers?.headers, [.init(name: "host", value: "graphql.com")])
guard case let .data(data) = request.body else {
XCTFail("Unexpected body")
return
}
XCTAssertEqual(data, dataObject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */; };
21F762A52BD6B1AA0048845A /* AuthSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5B727B61F0F006CCEC7 /* AuthSessionHelper.swift */; };
21F762A62BD6B1AA0048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEA828E747B80000C36A /* AsyncTesting.swift */; };
21F762A72BD6B1AA0048845A /* AuthSRPSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BE27B61F1D006CCEC7 /* AuthSRPSignInTests.swift */; };
Expand Down Expand Up @@ -169,6 +170,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncSignerTests.swift; sourceTree = "<group>"; };
21F762CB2BD6B1AA0048845A /* AuthGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthGen2IntegrationTests.xctestplan; sourceTree = "<group>"; };
4821B2F1286B5F74000EC1D7 /* AuthDeleteUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthDeleteUserTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -268,6 +270,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */ = {
isa = PBXGroup;
children = (
21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */,
);
path = AppSyncSignerTests;
sourceTree = "<group>";
};
4821B2F0286B5F74000EC1D7 /* AuthDeleteUserTests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -355,6 +365,7 @@
485CB5A027B61E04006CCEC7 /* AuthIntegrationTests */ = {
isa = PBXGroup;
children = (
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */,
21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */,
48916F362A412AF800E3E1B1 /* MFATests */,
97B370C32878DA3500F1C088 /* DeviceTests */,
Expand Down Expand Up @@ -851,6 +862,7 @@
681DFEAC28E747B80000C36A /* AsyncExpectation.swift in Sources */,
48E3AB3128E52590004EE395 /* GetCurrentUserTests.swift in Sources */,
48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */,
21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */,
485CB5B127B61EAC006CCEC7 /* AWSAuthBaseTest.swift in Sources */,
485CB5C027B61F1E006CCEC7 /* SignedOutAuthSessionTests.swift in Sources */,
485CB5BA27B61F10006CCEC7 /* AuthSignInHelper.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import Amplify
import AWSCognitoAuthPlugin

class AppSyncSignerTests: AWSAuthBaseTest {

/// Test signing an AppSync request with a live credentials provider
///
/// - Given: Base test configures Amplify and adds AWSCognitoAuthPlugin
/// - When:
/// - I invoke AWSCognitoAuthPlugin's AppSync signer
/// - Then:
/// - I should get a signed request.
///
func testSignAppSyncRequest() async throws {
let request = URLRequest(url: URL(string: "http://graphql.com")!)
let signer = AWSCognitoAuthPlugin.createAppSyncSigner(region: "us-east-1")
let signedRequest = try await signer(request)
guard let headers = signedRequest.allHTTPHeaderFields else {
XCTFail("Missing headers")
return
}
XCTAssertEqual(headers.count, 4)
let containsExpectedHeaders = headers.keys.contains(where: { key in
key == "Authorization" || key == "Host" || key == "X-Amz-Security-Token" || key == "X-Amz-Date"
})
XCTAssertTrue(containsExpectedHeaders)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
@_spi(InternalAmplifyConfiguration) import Amplify


/// Hold necessary AWS AppSync configuration values to interact with the AppSync API
public struct AWSAppSyncConfiguration {

/// The region of the AWS AppSync API
public let region: String

/// The endpoint of the AWS AppSync API
public let endpoint: URL

/// API key for API Key authentication.
public let apiKey: String?


/// Initializes an `AWSAppSyncConfiguration` instance using the provided AmplifyOutputs file.
/// AmplifyOutputs support multiple ways to read the `amplify_outputs.json` configuration file
///
/// For example, `try AWSAppSyncConfiguraton(with: .amplifyOutputs)` will read the
/// `amplify_outputs.json` file from the main bundle.
public init(with amplifyOutputs: AmplifyOutputs) throws {
let resolvedConfiguration = try amplifyOutputs.resolveConfiguration()

guard let dataCategory = resolvedConfiguration.data else {
throw ConfigurationError.invalidAmplifyOutputsFile(
"Missing data category", "", nil)
}

self.region = dataCategory.awsRegion
guard let endpoint = URL(string: dataCategory.url) else {
throw ConfigurationError.invalidAmplifyOutputsFile(
"Missing region from data category", "", nil)
}
self.endpoint = endpoint
self.apiKey = dataCategory.apiKey
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import AWSPluginsCore
@_spi(InternalAmplifyConfiguration) @testable import Amplify

final class AWSAppSyncConfigurationTests: XCTestCase {

func testSuccess() throws {
let config = AmplifyOutputsData(data: .init(
awsRegion: "us-east-1",
url: "http://www.example.com",
modelIntrospection: nil,
apiKey: "apiKey123",
defaultAuthorizationType: .amazonCognitoUserPools,
authorizationTypes: [.apiKey, .awsIAM]))
let encoder = JSONEncoder()
let data = try! encoder.encode(config)

let configuration = try AWSAppSyncConfiguration(with: .data(data))

XCTAssertEqual(configuration.region, "us-east-1")
XCTAssertEqual(configuration.endpoint, URL(string: "http://www.example.com")!)
XCTAssertEqual(configuration.apiKey, "apiKey123")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import AWSPluginsCore
import Foundation

public class AmplifyAWSCredentialsProvider: AWSClientRuntime.CredentialsProviding {

public func getCredentials() async throws -> AWSClientRuntime.AWSCredentials {
let authSession = try await Amplify.Auth.fetchAuthSession()
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {
Expand Down
2 changes: 1 addition & 1 deletion api-dump/AWSDataStorePlugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8205,7 +8205,7 @@
"-module",
"AWSDataStorePlugin",
"-o",
"\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.rjSPtedPzR\/AWSDataStorePlugin.json",
"\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.BZQxGLOyZz\/AWSDataStorePlugin.json",
"-I",
".build\/debug",
"-sdk-version",
Expand Down
Loading

0 comments on commit 673a075

Please sign in to comment.