Skip to content

MFA Transaction Signing

Enrol a user, set a PIN, and sign a transaction with a Secure Enclave key.

enrollForMfa, setMfaPin and signAction are the iOS core’s MFA flow. Read MFA Transaction Signing first for the three-call flow and what the token proves.

All three are async. Call them from a Task started by a user action, not from application(_: didFinishLaunchingWithOptions:).

Swift:

// 1. Once per (user, device), after login. No biometric prompt appears here.
let enrolment = await AlphynSDK.enrollForMfa(userId: userId)
guard enrolment.success else {
print("enrol failed: \(enrolment.errorCode ?? "UNKNOWN")")
return
}
// 2. Set or change the PIN. Verified and hashed ON THE SERVER.
var newPin = Array("1234".utf8)
defer { AlphynSDK.zeroize(&newPin) }
_ = await AlphynSDK.setMfaPin(userId: userId, newPin: newPin)
// 3. Per transaction, after the user clears your confirmation screen.
var pin = Array(enteredPin.utf8)
defer { AlphynSDK.zeroize(&pin) }
let result = await AlphynSDK.signAction(
userId: userId,
actionLabel: "TRANSFER",
context: ["amount": "250.00", "currency": "TRY", "toIBAN": "TR12…"],
pin: pin
)
if result.success, let token = result.verificationToken {
await sendToYourBackend(token)
}
public static func enrollForMfa(userId: String) async -> MfaResult
public static func setMfaPin(
userId: String, newPin: [UInt8], oldPin: [UInt8]? = nil
) async -> MfaResult
public static func signAction(
userId: String, actionLabel: String, context: [String: Any], pin: [UInt8]? = nil
) async -> ActionVerificationResult
public static func signAction(
userId: String, actionLabel: String, context: [String: Any], pin: [UInt8]? = nil,
subject: AlphynSubject?
) async -> ActionVerificationResult
public static func zeroize(_ pin: inout [UInt8])

A Swift String cannot be wiped: its storage is copy-on-write and may be copied anywhere before you are done with it. The MFA API therefore takes bytes, and AlphynSDK.zeroize(_:) overwrites your buffer in place. Build the bytes with Array(text.utf8), pass them, and zeroize in a defer so the wipe happens on every path including a thrown error.

The SDK zeroes its own copy as well. Yours is yours.

ParameterTypeNotes
userIdStringyour identifier for the logged-in user, not one Alphyn issues
newPin / oldPin[UInt8]UTF-8 bytes. oldPin is nil on a first set
actionLabelStringyour own name for the operation, e.g. TRANSFER
context[String: Any]the transaction. Its hash is bound into the signature
pin[UInt8]?optional. nil omits the factor rather than failing
subjectAlphynSubject?optional. nil is byte-identical to the four-parameter overload

success, errorCode, errorMessage, securityLevel, enrollmentId, isPinReady.

errorCode is the backend’s string verbatim and is never collapsed onto an enum, so a code this version of the SDK has never seen still reaches you.

success, verificationToken, expiresAtMs, errorCode, errorMessage. isTokenValid() reports whether the token is still within its window.

let subject = AlphynSubject.builder()
.tc("10000000146")
.phone("+905342223344")
.build()
let result = await AlphynSDK.signAction(
userId: userId, actionLabel: "TRANSFER", context: context, pin: pin, subject: subject)

build() never fails, for any input including nil. A field that does not conform is dropped, never corrected. tcHash, phoneHash and emailHash hold the tokens or nil; isEmpty reports whether anything survived, and an empty subject produces a request byte-identical to one that declared none.

The raw values never leave the device. See Subject identity.

AlphynSubject, its builder, and the three MFA entry points are exposed to the Objective-C runtime. An async Swift method appears there as a completion-handler method:

[AlphynSDK signActionWithUserId:userId
actionLabel:@"TRANSFER"
context:context
pin:pinBytes
subject:subject
completionHandler:^(ActionVerificationResult *result) { … }];

MfaResult and ActionVerificationResult expose -toJsonWithPrettyPrinted:error:, which is the supported way to read a result from Objective-C.

  • Signing a transfer, withdrawal or beneficiary change so your backend can prove afterwards that a specific user’s device authorised those exact values
  • Adding a second factor to a high-risk action without an SMS round trip
  • Correlating a signed transaction with a person across institutions, through a subject token
func confirmTransfer(userId: String, iban: String, amount: String, enteredPin: String) async {
var pin = Array(enteredPin.utf8)
defer { AlphynSDK.zeroize(&pin) }
let subject = AlphynSubject.builder().tc(customerTckn).build()
let result = await AlphynSDK.signAction(
userId: userId,
actionLabel: "TRANSFER",
context: ["amount": amount, "currency": "TRY", "toIBAN": iban],
pin: pin,
subject: subject
)
guard result.success, let token = result.verificationToken else {
// THREAT_DETECTED is a refusal, not a transport failure. Do not retry silently.
show(error: result.errorCode ?? "UNKNOWN")
return
}
await yourApi.transfer(iban: iban, amount: amount, token: token)
}

The token is evidence, not a decision. Your backend must validate it server-side and compare the echoed context against the transfer it is about to perform; see Backend verification.