Ping SDK - MFA OATH Module
PingOath is a comprehensive iOS SDK module that provides One-Time Password (OTP) authentication functionality, including support for both TOTP (Time-based One-Time Password) and HOTP (HMAC-based One-Time Password) authentication mechanisms following RFC 4226 and RFC 6238 standards.
Features
- OATH credential management (add, retrieve, delete)
- TOTP and HOTP support
- Multiple hashing algorithms (SHA1, SHA256, SHA512)
- Customizable digit lengths (6-8 digits)
- Customizable periods for TOTP
- URI parsing and formatting
- Secure iOS Keychain storage for credentials
- Policy-based credential locking and validation
Standard Compliance
- Full RFC 4226 (HOTP) and RFC 6238 (TOTP) compliance
- Configurable code length (6-8 digits) and time periods
- Base32 secret key encoding/decoding
Getting Started
Prerequisites
- iOS: 13.0+
- Swift: 5.7+
- Xcode: 15.0+
Installation
Swift Package Manager
Add PingOath to your Package.swift file:
dependencies: [
.package(url: "https://github.com/pingidentity/ping-ios-sdk", from: "1.0.0")
]
Or add it through Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Select PingOath module
CocoaPods
pod 'PingOath'
Usage
Initialize the OATH Client
Before using the OATH MFA functionality, you need to initialize the OathClient. There are several ways to create and initialize an OathClient:
Basic Initialization
import PingOath
// Create an OATH client
let client = try await OathClient.createClient { config in
config.logger = LogManager.logger
config.enableCredentialCache = false
}
// Add a credential from URI (e.g., QR code scan)
let credential = try await client.addCredentialFromUri(
"otpauth://totp/Example:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
)
// Generate OTP code
let code = try await client.generateCode(credential.id)
print("OTP Code: \(code)")
// Clean up when done
try await client.close()
Advanced Configuration
let client = try await OathClient.createClient { config in
// Use custom storage
config.storage = OathKeychainStorage(
service: "com.myapp.oath",
accessibility: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
)
// Enable caching for performance
config.enableCredentialCache = true
// Configure logging
config.logger = MyCustomLogger()
// Set network timeout
config.timeoutMs = 30.0
// Enable data encryption
config.encryptionEnabled = true
}
Add a Credential from URI
Add a new OATH credential from a URI:
// HOTP credentials automatically increment counter
let hotpUri = "otpauth://hotp/Example:user@example.com?secret=JBSWY3DPEHPK3PXP&counter=0"
let hotpCredential = try await client.addCredentialFromUri(hotpUri)
Generate OTP Code
Generate a one-time password for an OATH credential:
HOTP
// Generate code (counter increments automatically)
let code1 = try await client.generateCode(hotpCredential.id) // Counter = 1
let code2 = try await client.generateCode(hotpCredential.id) // Counter = 2
TOTP
// Get code with timing and validity information
let codeInfo = try await client.generateCodeWithValidity(credential.id)
print("Code: \(codeInfo.code)")
print("Time remaining: \(codeInfo.timeRemaining) seconds")
print("Progress: \(codeInfo.progress * 100)%")
Storage Options
OathKeychainStorage (Default)
Secure storage using iOS Keychain Services:
let storage = OathKeychainStorage(
service: "com.myapp.oath", // Keychain service
accessGroup: "group.myapp", // Shared keychain access
accessibility: kSecAttrAccessibleWhenUnlockedThisDeviceOnly // Security level
)
Custom Storage
Implement the OathStorage protocol for custom storage solutions:
class MyCustomStorage: OathStorage {
func storeOathCredential(_ credential: OathCredential) async throws { }
func retrieveOathCredential(credentialId: String) async throws -> OathCredential? { }
func getAllOathCredentials() async throws -> [OathCredential] { }
func removeOathCredential(credentialId: String) async throws -> Bool { }
func clearOathCredentials() async throws { }
}
URI Formats
Supported URI Schemes
TOTP (Time-based)
otpauth://totp/Issuer:Account?secret=SECRET&issuer=Issuer&algorithm=SHA1&digits=6&period=30
HOTP (Counter-based)
otpauth://hotp/Issuer:Account?secret=SECRET&issuer=Issuer&algorithm=SHA1&digits=6&counter=0
MFA Extensions
mfauth://totp/Issuer:Account?secret=SECRET&issuer=Issuer&uid=USER_ID&oid=DEVICE_ID
URI Parameters
| Parameter | Required | Description | Default |
|---|---|---|---|
secret |
✅ | Base32 encoded secret key | - |
issuer |
❌ | Service provider name | - |
algorithm |
❌ | HMAC algorithm (SHA1/SHA256/SHA512) | SHA1 |
digits |
❌ | Code length (4-8) | 6 |
period |
❌ | TOTP validity period in seconds | 30 |
counter |
❌ | HOTP counter value | 0 |
uid |
❌ | Base64 encoded user ID | - |
oid |
❌ | Device/resource ID | - |
image |
❌ | Logo URL | - |
Error Handling
Error Types
The SDK provides comprehensive error handling:
do {
let credential = try await client.addCredentialFromUri(uri)
} catch let error as OathError {
switch error {
case .invalidUri(let message):
print("Invalid URI: \(message)")
case .credentialNotFound(let id):
print("Credential not found: \(id)")
case .credentialLocked(let id):
print("Credential is locked: \(id)")
case .codeGenerationFailed(let message, let underlying):
print("Code generation failed: \(message)")
case .initializationFailed(let message, let underlying):
print("Client initialization failed: \(message)")
}
} catch let error as OathStorageError {
switch error {
case .storageFailure(let message, let underlying):
print("Storage error: \(message)")
}
}
License
© Copyright 2025-2026 Ping Identity Corporation. All Rights Reserved This software may be modified and distributed under the terms of the MIT license. See the LICENSE file for details.
View on GitHub