Skip to content
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 @@ -902,7 +902,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.amazon.aws.amplify.swift.AnalyticsHostAppWatch.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SKIP_INSTALL = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
Expand Down Expand Up @@ -932,7 +932,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.amazon.aws.amplify.swift.AnalyticsHostAppWatch.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SKIP_INSTALL = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ extension PlatformWebAuthnCredentials: CredentialRegistrantProtocol {
name: options.user.name,
userID: options.user.id
)
#if !os(visionOS)
// `excludedCredentials` is not available on visionOS
platformKeyRequest.excludedCredentials = options.excludeCredentials.compactMap { credential in
return .init(credentialID: credential.id)
}
#endif

return try await withCheckedThrowingContinuation { continuation in
registrationContinuation = continuation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import XCTest

final class AuthWebAuthnAppUITests: XCTestCase {
private let timeout = TimeInterval(6)
private let timeout = TimeInterval(30)
private let app = XCUIApplication()
private var username: String!
private var signUpButton: XCUIElement!
Expand Down Expand Up @@ -72,20 +72,14 @@ final class AuthWebAuthnAppUITests: XCTestCase {
@MainActor
func testWebAuthnAPIs() async throws {
// 1. Associate new WebAuthn Credential
let associateContinueButton = springboard.otherElements["ASAuthorizationControllerContinueButton"]
let associateAttempt = await attempt {
associateButton.tap()
return !waitForResult("Associate WebAuthn Credential failed:", timeout: 1)
return associateContinueButton.waitForExistence(timeout: timeout)
}

guard associateAttempt else {
XCTFail("Failed to trigger the Associate WebAuthn Credential workflow: \(lastResult)")
return
}

// Wait for the "Continue" button to appear in the FaceID popover and tap it
let associateContinueButton = springboard.otherElements["ASAuthorizationControllerContinueButton"]
guard associateContinueButton.waitForExistence(timeout: timeout) else {
XCTFail("Failed to find the 'Continue' button to Associate new WebAuthn credential")
XCTFail("Failed to find the 'Continue' button to Associate new WebAuthn credential: \(lastResult)")
return
}
associateContinueButton.tap()
Expand All @@ -112,20 +106,14 @@ final class AuthWebAuthnAppUITests: XCTestCase {
}

// 4. Sign in with WebAuthn
let signInContinueButton = springboard.otherElements["ASAuthorizationControllerContinueButton"]
let signInAttempt = await attempt {
signInButton.tap()
return !waitForResult("Sign In failed:", timeout: 1)
return signInContinueButton.waitForExistence(timeout: timeout)
}

guard signInAttempt else {
XCTFail("Failed to trigger the Assert WebAuthn Credential workflow: \(lastResult)")
return
}

// Wait for the "Continue" button to appear in the FaceID popover
let signInContinueButton = springboard.otherElements["ASAuthorizationControllerContinueButton"]
guard signInContinueButton.waitForExistence(timeout: timeout) else {
XCTFail("Failed to find the 'Continue' button to Sign In with WebAuthn")
XCTFail("Failed to find the 'Continue' button to Sign In with WebAuthn: \(lastResult)")
return
}

Expand Down
41 changes: 30 additions & 11 deletions AmplifyPlugins/Auth/Tests/AuthWebAuthnApp/LocalServer/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@ app.use(express.json())

const bundleId = "com.amazon.aws.amplify.swift.AuthWebAuthnApp"

const run = (cmd) => {
// Simulator device identifiers are either a UUID (UDID) or the literal "booted".
// Validating up front rejects any value that could be used to smuggle shell
// metacharacters into the commands below.
const deviceIdPattern = /^([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}|booted)$/

const isValidDeviceId = (deviceId) => typeof deviceId === "string" && deviceIdPattern.test(deviceId)

// Run a command without invoking a shell. Arguments are passed as an array so
// user-supplied values (e.g. deviceId) are never interpreted by /bin/sh,
// preventing command injection.
const run = (file, args) => {
return new Promise((resolve, reject) => {
childProcess.exec(cmd, (error, stdout, stderror) => {
childProcess.execFile(file, args, (error, stdout, stderror) => {
if (error) {
console.warn("Failed to execute cmd:", cmd)
console.warn("Failed to execute:", file, args)
reject(stderror)
} else {
resolve(stdout)
Expand All @@ -22,9 +32,11 @@ const run = (cmd) => {
app.post('/uninstall', async (req, res) => {
console.log("POST /uninstall ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl uninstall ${deviceId} ${bundleId}`
await run(cmd)
await run("xcrun", ["simctl", "uninstall", deviceId, bundleId])
res.send("Done")
} catch (error) {
console.error("Failed to uninstall app", error)
Expand All @@ -35,9 +47,11 @@ app.post('/uninstall', async (req, res) => {
app.post('/boot', async (req, res) => {
console.log("POST /boot ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl bootstatus ${deviceId} -b`
await run(cmd)
await run("xcrun", ["simctl", "bootstatus", deviceId, "-b"])
res.send("Done")
} catch (error) {
console.error("Failed to boot the device", error)
Expand All @@ -48,9 +62,12 @@ app.post('/boot', async (req, res) => {
app.post('/enroll', async (req, res) => {
console.log("POST /enroll ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl spawn ${deviceId} notifyutil -s com.apple.BiometricKit.enrollmentChanged '1' && xcrun simctl spawn ${deviceId} notifyutil -p com.apple.BiometricKit.enrollmentChanged`
await run(cmd)
await run("xcrun", ["simctl", "spawn", deviceId, "notifyutil", "-s", "com.apple.BiometricKit.enrollmentChanged", "1"])
await run("xcrun", ["simctl", "spawn", deviceId, "notifyutil", "-p", "com.apple.BiometricKit.enrollmentChanged"])
res.send("Done")
} catch (error) {
console.error("Failed to enroll biometrics in the device", error)
Expand All @@ -62,9 +79,11 @@ app.post('/enroll', async (req, res) => {
app.post('/match', async (req, res) => {
console.log("POST /match ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl spawn ${deviceId} notifyutil -p com.apple.BiometricKit_Sim.fingerTouch.match`
await run(cmd)
await run("xcrun", ["simctl", "spawn", deviceId, "notifyutil", "-p", "com.apple.BiometricKit_Sim.fingerTouch.match"])
res.send("Done")
} catch (error) {
console.error("Failed to match biometrics", error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public protocol AWSCredentialsProvider {
- accessKeyId: A unique identifier.
- secretAccessKey: A secret key used to sign requests cryptographically.
*/
public protocol AWSCredentials {
public protocol AWSCredentials: Sendable {

/// A unique identifier.
var accessKeyId: String { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@ app.use(express.json())

const bundleId = "com.aws.amplify.notification.PushNotificationHostApp"

const run = (cmd) => {
// Simulator device identifiers are either a UUID (UDID) or the literal "booted".
// Validating up front rejects any value that could be used to smuggle shell
// metacharacters into the commands below.
const deviceIdPattern = /^([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}|booted)$/

const isValidDeviceId = (deviceId) => typeof deviceId === "string" && deviceIdPattern.test(deviceId)

// Run a command without invoking a shell. Arguments are passed as an array so
// user-supplied values (e.g. deviceId) are never interpreted by /bin/sh,
// preventing command injection.
const run = (file, args) => {
return new Promise((resolve, reject) => {
childProcess.exec(cmd, (error, stdout, stderror) => {
childProcess.execFile(file, args, (error, stdout, stderror) => {
if (error) {
console.warn("Failed to execute cmd:", cmd)
console.warn("Failed to execute:", file, args)
reject(stderror)
} else {
resolve(stdout)
Expand All @@ -19,6 +29,23 @@ const run = (cmd) => {
})
}

// Run a command without a shell and feed the given string to its stdin.
// Used in place of `echo '<json>' | xcrun simctl push ... -` so the payload
// never passes through a shell.
const runWithStdin = (file, args, input) => {
return new Promise((resolve, reject) => {
const child = childProcess.execFile(file, args, (error, stdout, stderror) => {
if (error) {
console.warn("Failed to execute:", file, args)
reject(stderror)
} else {
resolve(stdout)
}
})
child.stdin.end(input)
})
}

/**
* Trigger a new push notification.
* Run `xcrun simctl push ...` command under the hood
Expand All @@ -35,6 +62,10 @@ app.post("/notifications", async (req, res) => {
deviceId
} = req.body

if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}

const apns = {
aps: {
alert: {
Expand All @@ -46,8 +77,9 @@ app.post("/notifications", async (req, res) => {
data: data ?? {}
}
try {
const cmd = `echo '${JSON.stringify(apns)}' | xcrun simctl push ${deviceId} ${bundleId} -`
await run(cmd)
// Read the payload from stdin ("-") rather than interpolating it into a
// shell pipeline.
await runWithStdin("xcrun", ["simctl", "push", deviceId, bundleId, "-"], JSON.stringify(apns))
res.send("Done")
} catch (error) {
console.log("Failed to trigger notification", error)
Expand All @@ -59,9 +91,11 @@ app.post("/notifications", async (req, res) => {
app.post('/uninstall', async (req, res) => {
console.log("POST /uninstall ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl uninstall ${deviceId} ${bundleId}`
await run(cmd)
await run("xcrun", ["simctl", "uninstall", deviceId, bundleId])
res.send("Done")
} catch (error) {
console.error("Failed to uninstall app", error)
Expand All @@ -72,9 +106,11 @@ app.post('/uninstall', async (req, res) => {
app.post('/boot', async (req, res) => {
console.log("POST /boot ")
const { deviceId } = req.body
if (!isValidDeviceId(deviceId)) {
return res.status(400).send("Invalid deviceId")
}
try {
const cmd = `xcrun simctl bootstatus ${deviceId} -b`
await run(cmd)
await run("xcrun", ["simctl", "bootstatus", deviceId, "-b"])
res.send("Done")
} catch (error) {
console.error("Failed to boot the device", error)
Expand All @@ -84,4 +120,4 @@ app.post('/boot', async (req, res) => {

app.listen(9293, () => {
console.log("Starting server")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.notification.PushNotificationHostApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SKIP_INSTALL = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
Expand Down Expand Up @@ -899,7 +899,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.notification.PushNotificationHostApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SKIP_INSTALL = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,13 @@ struct SigV4Signer {
serviceName: serviceName
)

let existingQuery = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.map { "\($0.name)=\($0.value ?? "")" }
.joined(separator: "&")

let canonicalQueryString = _canonicalQueryString(
query: url.query,
query: existingQuery,
signedHeaders: signedHeaders,
timestamp: timestamp,
credentialScope: credentialScope,
Expand Down Expand Up @@ -325,7 +330,7 @@ struct SigV4Signer {

let sorted = canonicalQueryString.split(separator: "&")
.map {
String($0).split(separator: "=")
String($0).split(separator: "=", maxSplits: 1)
.map(String.init)
.map(PercentEncoding.uri.encode)
.joined(separator: "=")
Expand Down
Loading
Loading