Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attestation event logging to FC API clients #4481

Merged
merged 8 commits into from
Jan 27, 2025
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
20 changes: 10 additions & 10 deletions StripeCore/StripeCore/Source/Attestation/StripeAttest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,25 +113,25 @@ import UIKit
}
}

@_spi(STP) public enum AttestationError: Error {
@_spi(STP) public enum AttestationError: String, Error {
/// Attestation is not supported on this device.
case attestationNotSupported
case attestationNotSupported = "attestation_not_supported"
/// Device ID is unavailable.
case noDeviceID
case noDeviceID = "no_device_id"
/// App ID is unavailable.
case noAppID
case noAppID = "no_app_id"
/// Retried assertion, but it failed.
case secondAssertionFailureAfterRetryingAttestation
case secondAssertionFailureAfterRetryingAttestation = "second_assertion_failure_after_retrying_attestation"
/// Can't attest any more keys today.
case attestationRateLimitExceeded
case attestationRateLimitExceeded = "attestation_rate_limit_exceeded"
/// The challenge couldn't be converted to UTF-8 data.
case invalidChallengeData
case invalidChallengeData = "invalid_challenge_data"
/// The backend asked us not to attest
case shouldNotAttest
case shouldNotAttest = "should_not_attest"
/// The backend asked us to attest, but the key is already attested
case shouldAttestButKeyIsAlreadyAttested
case shouldAttestButKeyIsAlreadyAttested = "should_attest_but_key_is_already_attested"
/// A publishable key was not set
case noPublishableKey
case noPublishableKey = "no_publishable_key"
}

// MARK: - Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */; };
496A6AE72C29E0BB00D34F8E /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 496A6AE62C29E0BB00D34F8E /* [email protected] */; };
497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */; };
499EEAFD2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */; };
49A0B5862C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */; };
49AC518C2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AC518B2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift */; };
49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */; };
Expand Down Expand Up @@ -330,6 +331,7 @@
495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsTheme.swift; sourceTree = "<group>"; };
496A6AE62C29E0BB00D34F8E /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
497142BB2C514B08000DFA64 /* FlowRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowRouterTests.swift; sourceTree = "<group>"; };
499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientLogger.swift; sourceTree = "<group>"; };
49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientTests.swift; sourceTree = "<group>"; };
49AC518B2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsLinkLoginPane.swift; sourceTree = "<group>"; };
49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLoginDataSource.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -657,6 +659,7 @@
4E2EAD7059FF8358E674774A /* FinancialConnectionsAPIClient.swift */,
49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */,
49F1B83D2D2EC82300136303 /* FinancialConnectionsAsyncAPIClient+Legacy.swift */,
499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */,
);
path = "API Bindings";
sourceTree = "<group>";
Expand Down Expand Up @@ -1319,6 +1322,7 @@
CBF7BE2271D309F2B1E794CC /* FinancialConnectionsDataAccessNotice.swift in Sources */,
F67624595BD2CD7B6793BFDA /* FinancialConnectionsImage.swift in Sources */,
07712610C7D2F484AAB96982 /* FinancialConnectionsInstitution.swift in Sources */,
499EEAFD2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift in Sources */,
7386E1F9256B23CE29BF996D /* FinancialConnectionsInstitutionSearchResultResource.swift in Sources */,
C7D2763ACCE2CC71E788E18F /* FinancialConnectionsLegalDetailsNotice.swift in Sources */,
B271AAF41C9FE6AE392B88D3 /* FinancialConnectionsMixedOAuthParams.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ final class FinancialConnectionsAPIClient {
var consumerPublishableKey: String?
var consumerSession: ConsumerSessionData?

private lazy var logger = FinancialConnectionsAPIClientLogger()

var requestSurface: String {
isLinkWithStripe ? "ios_instant_debits" : "ios_connections"
}
Expand All @@ -46,28 +48,37 @@ final class FinancialConnectionsAPIClient {
/// Applies attestation-related parameters to the given base parameters
/// In case of an assertion error, returns the unmodified base parameters
func assertAndApplyAttestationParameters(
to baseParameters: [String: Any]
to baseParameters: [String: Any],
api: FinancialConnectionsAPIClientLogger.API,
pane: FinancialConnectionsSessionManifest.NextPane
) -> Future<[String: Any]> {
let promise = Promise<[String: Any]>()
Task {
do {
let attest = backingAPIClient.stripeAttest
let handle = try await attest.assert()
logger.log(.attestationRequestTokenSucceeded(api), pane: pane)
let newParameters = baseParameters.merging(handle.assertion.requestFields) { (_, new) in new }
promise.resolve(with: newParameters)
} catch {
// Fail silently if we can't get an assertion, we'll try the request anyway. It may fail.
logger.log(.attestationRequestTokenFailed(api, error), pane: pane)
promise.resolve(with: baseParameters)
}
}
return promise
}

/// Marks the assertion as completed and forwards attestation errors to the `StripeAttest` client for logging.
func completeAssertion(possibleError: Error?) {
func completeAssertion(
possibleError: Error?,
api: FinancialConnectionsAPIClientLogger.API,
pane: FinancialConnectionsSessionManifest.NextPane
) {
let attest = backingAPIClient.stripeAttest
Task {
if let error = possibleError, StripeAttest.isLinkAssertionError(error: error) {
logger.log(.attestationVerdictFailed(api), pane: pane)
await attest.receivedAssertionError(error)
}
await attest.assertionCompleted()
Expand Down Expand Up @@ -139,11 +150,16 @@ protocol FinancialConnectionsAPI {
var consumerPublishableKey: String? { get set }
var consumerSession: ConsumerSessionData? { get set }

func completeAssertion(possibleError: Error?)
func completeAssertion(
possibleError: Error?,
api: FinancialConnectionsAPIClientLogger.API,
pane: FinancialConnectionsSessionManifest.NextPane
)

func synchronize(
clientSecret: String,
returnURL: String?
returnURL: String?,
initialSynchronize: Bool
) -> Future<FinancialConnectionsSynchronize>

func fetchFinancialConnectionsAccounts(
Expand Down Expand Up @@ -256,7 +272,8 @@ protocol FinancialConnectionsAPI {
clientSecret: String,
sessionId: String,
emailSource: FinancialConnectionsAPIClient.EmailSource,
useMobileEndpoints: Bool
useMobileEndpoints: Bool,
pane: FinancialConnectionsSessionManifest.NextPane
) -> Future<LookupConsumerSessionResponse>

// MARK: - Link API's
Expand Down Expand Up @@ -285,7 +302,8 @@ protocol FinancialConnectionsAPI {
amount: Int?,
currency: String?,
incentiveEligibilitySession: ElementsSessionContext.IntentID?,
useMobileEndpoints: Bool
useMobileEndpoints: Bool,
pane: FinancialConnectionsSessionManifest.NextPane
) -> Future<LinkSignUpResponse>

func attachLinkConsumerToLinkAccountSession(
Expand Down Expand Up @@ -348,7 +366,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {

func synchronize(
clientSecret: String,
returnURL: String?
returnURL: String?,
initialSynchronize: Bool = false
) -> Future<FinancialConnectionsSynchronize> {
var parameters: [String: Any] = [
"expand": ["manifest.active_auth_session"],
Expand All @@ -363,9 +382,14 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
]
mobileParameters["app_return_url"] = returnURL

let attest = backingAPIClient.stripeAttest
mobileParameters["supports_app_verification"] = attest.isSupported
mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier
if initialSynchronize {
let attestationIsSupported = backingAPIClient.stripeAttest.isSupported
mobileParameters["supports_app_verification"] = attestationIsSupported
mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier
if !attestationIsSupported {
logger.log(.attestationInitFailed, pane: .consent)
}
}

parameters["mobile"] = mobileParameters
return self.post(
Expand Down Expand Up @@ -915,7 +939,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
clientSecret: String,
sessionId: String,
emailSource: FinancialConnectionsAPIClient.EmailSource,
useMobileEndpoints: Bool
useMobileEndpoints: Bool,
pane: FinancialConnectionsSessionManifest.NextPane
) -> Future<LookupConsumerSessionResponse> {
var parameters: [String: Any] = [
"email_address":
Expand All @@ -928,17 +953,20 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
parameters["request_surface"] = requestSurface
parameters["session_id"] = sessionId
parameters["email_source"] = emailSource.rawValue
return assertAndApplyAttestationParameters(to: parameters)
.chained { [weak self] updatedParameters in
guard let self else {
return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated."))
}
return self.post(
resource: APIMobileEndpointConsumerSessionLookup,
parameters: updatedParameters,
useConsumerPublishableKeyIfNeeded: false
)
return assertAndApplyAttestationParameters(
to: parameters,
api: .consumerSessionLookup,
pane: pane
).chained { [weak self] updatedParameters in
guard let self else {
return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated."))
}
return self.post(
resource: APIMobileEndpointConsumerSessionLookup,
parameters: updatedParameters,
useConsumerPublishableKeyIfNeeded: false
)
}
} else {
parameters["client_secret"] = clientSecret
return post(
Expand Down Expand Up @@ -1001,7 +1029,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
amount: Int?,
currency: String?,
incentiveEligibilitySession: ElementsSessionContext.IntentID?,
useMobileEndpoints: Bool
useMobileEndpoints: Bool,
pane: FinancialConnectionsSessionManifest.NextPane
) -> Future<LinkSignUpResponse> {
var parameters: [String: Any] = [
"request_surface": requestSurface,
Expand Down Expand Up @@ -1038,16 +1067,19 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
}

if useMobileEndpoints {
return assertAndApplyAttestationParameters(to: parameters)
.chained { [weak self] updatedParameters in
guard let self else {
return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated."))
}
return self.post(
resource: APIMobileEndpointLinkAccountSignUp,
parameters: updatedParameters,
useConsumerPublishableKeyIfNeeded: false
)
return assertAndApplyAttestationParameters(
to: parameters,
api: .linkSignUp,
pane: pane
).chained { [weak self] updatedParameters in
guard let self else {
return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated."))
}
return self.post(
resource: APIMobileEndpointLinkAccountSignUp,
parameters: updatedParameters,
useConsumerPublishableKeyIfNeeded: false
)
}
} else {
return post(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// FinancialConnectionsAPIClientLogger.swift
// StripeFinancialConnections
//
// Created by Mat Schmid on 2025-01-20.
//

import Foundation
@_spi(STP) import StripeCore

struct FinancialConnectionsAPIClientLogger {
private var analyticsClient = FinancialConnectionsAnalyticsClient()

enum API: String {
case consumerSessionLookup = "consumer_session_lookup"
case linkSignUp = "link_sign_up"
}

enum Event {
/// When checking if generating attestation is supported does not succeed.
case attestationInitFailed
/// When an attestation token gets generated successfully.
case attestationRequestTokenSucceeded(API)
/// When a token generation attempt fails client-side.
case attestationRequestTokenFailed(API, Error)
/// When an attestation verdict fails backend side and we get an attestation related error.
case attestationVerdictFailed(API)

var name: String {
switch self {
case .attestationInitFailed:
return "attestation.init_failed"
case .attestationRequestTokenSucceeded:
return "attestation.request_token_succeeded"
case .attestationRequestTokenFailed:
return "attestation.request_token_failed"
case .attestationVerdictFailed:
return "attestation.verdict_failed"
}
}

var parameters: [String: Any] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosmuvi-stripe let me know which other parameters we should include for each event

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be good with these + the default parameters sent on all requests

switch self {
case .attestationInitFailed:
var reason: String
if #available(iOS 14.0, *) {
// If the iOS version is supported, we assume the device is unsupported (i.e. simulator).
reason = "ios_device_unsupported"
} else {
// Otherwise, attestation is unavailable due to the OS version being unsupported.
reason = "ios_os_version_unsupported"
}
return ["reason": reason]
case .attestationRequestTokenFailed(let api, let error):
var errorReason: String
if let attestationError = error as? StripeAttest.AttestationError {
errorReason = attestationError.rawValue
} else {
errorReason = "unknown"
}
return [
"api": api.rawValue,
"error_reason": errorReason,
]
case .attestationRequestTokenSucceeded(let api), .attestationVerdictFailed(let api):
return ["api": api.rawValue]
}
}
}

func log(_ event: Event, pane: FinancialConnectionsSessionManifest.NextPane) {
analyticsClient.log(
eventName: event.name,
parameters: event.parameters,
pane: pane
)
}
}
Loading
Loading