MFA Transaction Signing
Enrol a user, set a PIN, and sign a transaction with a Secure Enclave key.
Overview
Section titled “Overview”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:).
Basic Usage
Section titled “Basic Usage”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)}Methods
Section titled “Methods”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])Why the PIN is [UInt8] and not String
Section titled “Why the PIN is [UInt8] and not String”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.
Parameters
Section titled “Parameters”| Parameter | Type | Notes |
|---|---|---|
userId | String | your identifier for the logged-in user, not one Alphyn issues |
newPin / oldPin | [UInt8] | UTF-8 bytes. oldPin is nil on a first set |
actionLabel | String | your 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 |
subject | AlphynSubject? | optional. nil is byte-identical to the four-parameter overload |
MfaResult
Section titled “MfaResult”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.
ActionVerificationResult
Section titled “ActionVerificationResult”success, verificationToken, expiresAtMs, errorCode, errorMessage. isTokenValid() reports
whether the token is still within its window.
Declaring an identity
Section titled “Declaring an identity”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.
Objective-C
Section titled “Objective-C”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.
Use Cases
Section titled “Use Cases”- 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
Example: signing a transfer end to end
Section titled “Example: signing a transfer end to end”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.