PingDeviceClient - iOS SDK
Comprehensive device management SDK for Ping AIC with Result-based API for robust error handling.
Overview
PingDeviceClient module simplifies device management operations for Ping AIC. It provides a clean, type-safe, Result-based API for managing authentication devices including OATH, Push, Bound, Profile, and WebAuthn devices.
Features
Supported Device Types
| Device Type | Operations | Description |
|---|---|---|
| Oath | Read, Update, Delete | TOTP/HOTP authenticator devices |
| Push | Read, Update, Delete | Push notification devices |
| Bound | Read, Update, Delete | Device binding for 2FA |
| Profile | Read, Update, Delete | Device profiling data |
| WebAuthn | Read, Update, Delete | FIDO2/WebAuthn credentials |
Core Capabilities
- ✅ Fetch all devices for a user by type
- ✅ Update device properties (name)
- ✅ Delete devices
- ✅ Complex metadata handling
- ✅ Location data support
- ✅ Async/await throughout
- ✅ Result-based error handling
- ✅ Automatic session management with caching
- ✅ Thread-safe operations
Installation
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/ForgeRock/ping-ios-sdk", from: "1.3.0")
]
CocoaPods
pod 'PingDeviceClient', '~> 1.3.0'
Quick Start
1. Import the SDK
import PingDeviceClient
2. Configure and Initialize
// Obtain session token from your authentication flow
let sessionToken = "AQIC5w..." // From successful login
// Create configuration (minimal required parameters)
let config = DeviceClientConfig(
serverUrl: "https://openam.example.com",
ssoToken: sessionToken
)
// Initialize client
let deviceClient = DeviceClient(config: config)
3. Perform Operations with Result API
// Fetch devices - returns Result
let result = await deviceClient.oath.get()
switch result {
case .success(let devices):
print("Found \(devices.count) OATH devices")
for device in devices {
print("- \(device.deviceName)")
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
if let suggestion = error.recoverySuggestion {
print("Suggestion: \(suggestion)")
}
}
// Update a device
if case .success(var devices) = await deviceClient.bound.get(),
var device = devices.first {
device.deviceName = "My Updated Device"
let updateResult = await deviceClient.bound.update(device)
if case .success = updateResult {
print("Device updated successfully")
}
}
// Delete a device
if case .success(let devices) = await deviceClient.oath.get(),
let device = devices.first {
let deleteResult = await deviceClient.oath.delete(device)
if case .success = deleteResult {
print("Device deleted successfully")
}
}
Configuration
DeviceClientConfig
The configuration struct contains parameters needed for device management:
public struct DeviceClientConfig {
/// Base URL of the ForgeRock/Ping server
/// Example: "https://openam.example.com"
let serverUrl: String
/// Realm for authentication (default: "root")
/// Example: "alpha", "root"
let realm: String
/// HTTP header name for session token (default: "iPlanetDirectoryPro")
let cookieName: String
/// SSO session token
/// Must be valid and non-expired
let ssoToken: String
/// HTTP client (optional)
let httpClient: HttpClient
}
Configuration Examples
Basic Configuration (Using Defaults)
let config = DeviceClientConfig(
serverUrl: "https://openam.example.com",
ssoToken: sessionToken
)
// Uses defaults:
// - realm: "root"
// - cookieName: "iPlanetDirectoryPro"
Full Configuration
let config = DeviceClientConfig(
serverUrl: "https://openam.example.com",
realm: "alpha",
cookieName: "iPlanetDirectoryPro",
ssoToken: sessionToken
)
With Custom HTTP Client
let customHttpClient = HttpClient()
customHttpClient.timeoutIntervalForRequest = 30
let config = DeviceClientConfig(
serverUrl: "https://openam.example.com",
realm: "alpha",
cookieName: "iPlanetDirectoryPro",
ssoToken: sessionToken,
httpClient: customHttpClient
)
Automatic User ID Fetching
DeviceClient automatically fetches the user ID from the session endpoint on first use and caches it for subsequent requests. You don’t need to provide or manage the user ID manually.
// First operation - fetches userId from session endpoint
let result1 = await deviceClient.oath.get() // Makes 2 calls: session + devices
// Subsequent operations - uses cached userId
let result2 = await deviceClient.push.get() // Makes 1 call: devices only
Usage
Fetching Devices (Result API)
// Oath devices (authenticator apps)
let result = await deviceClient.oath.get()
switch result {
case .success(let devices):
for device in devices {
print("Device: \(device.deviceName)")
print(" UUID: \(device.uuid)")
print(" Created: \(Date(timeIntervalSince1970: device.createdDate))")
}
case .failure(let error):
handleError(error)
}
// Other device types
let pushResult = await deviceClient.push.get()
let boundResult = await deviceClient.bound.get()
let profileResult = await deviceClient.profile.get()
let webAuthnResult = await deviceClient.webAuthn.get()
Updating Devices
All device types support updates:
// Update a Bound device
let fetchResult = await deviceClient.bound.get()
if case .success(var devices) = fetchResult,
var device = devices.first {
device.deviceName = "My iPhone 15"
let updateResult = await deviceClient.bound.update(device)
switch updateResult {
case .success:
print("Device updated successfully")
case .failure(let error):
print("Update failed: \(error.localizedDescription)")
}
}
// Update a Profile device
if case .success(var devices) = await deviceClient.profile.get(),
var device = devices.first {
device.deviceName = "Updated Profile"
let result = await deviceClient.profile.update(device)
if case .success = result {
print("Profile updated")
}
}
// Update a WebAuthn device
if case .success(var devices) = await deviceClient.webAuthn.get(),
var device = devices.first {
device.deviceName = "YubiKey 5C"
await deviceClient.webAuthn.update(device)
}
Deleting Devices
All device types support deletion:
// Delete an Oath device
let fetchResult = await deviceClient.oath.get()
if case .success(let devices) = fetchResult,
let device = devices.first {
let deleteResult = await deviceClient.oath.delete(device)
switch deleteResult {
case .success:
print("Device deleted")
case .failure(let error):
print("Delete failed: \(error)")
}
}
// Delete other device types
await deviceClient.push.delete(pushDevice)
await deviceClient.bound.delete(boundDevice)
await deviceClient.profile.delete(profileDevice)
await deviceClient.webAuthn.delete(webAuthnDevice)
Device Types
Oath Device
struct OathDevice: Device {
let id: String
var deviceName: String // Mutable
let uuid: String
let createdDate: TimeInterval
let lastAccessDate: TimeInterval
let urlSuffix: String
}
// Usage
let result = await client.oath.get()
if case .success(let devices) = result {
for device in devices {
print("\(device.deviceName): \(device.uuid)")
}
}
Push Device
struct PushDevice: Device {
let id: String
var deviceName: String // Mutable
let uuid: String
let createdDate: TimeInterval
let lastAccessDate: TimeInterval
let urlSuffix: String
}
Bound Device
struct BoundDevice: Device {
let id: String
var deviceName: String // Mutable
let deviceId: String
let uuid: String
let createdDate: TimeInterval
let lastAccessDate: TimeInterval
let urlSuffix: String
}
Profile Device
struct ProfileDevice: Device {
let id: String
var deviceName: String // Mutable
let identifier: String
let metadata: [String: any Sendable] // Complex metadata
let location: Location?
let lastSelectedDate: TimeInterval
let urlSuffix: String
}
struct Location: Codable {
let latitude: Double
let longitude: Double
}
// Usage - Access metadata
let result = await client.profile.get()
if case .success(let devices) = result, let device = devices.first {
print("Platform: \(device.metadata["platform"] as? String ?? "Unknown")")
if let location = device.location {
print("Location: \(location.latitude), \(location.longitude)")
}
}
WebAuthn Device
struct WebAuthnDevice: Device {
let id: String
var deviceName: String // Mutable
let credentialId: String
let uuid: String
let createdDate: TimeInterval
let lastAccessDate: TimeInterval
let urlSuffix: String
}
Error Handling
Result-Based Error Handling
All operations return Result<Success, DeviceError>:
let result = await deviceClient.oath.get()
switch result {
case .success(let devices):
// Handle success
processDevices(devices)
case .failure(let error):
// Handle error
switch error {
case .networkError(let underlyingError):
print("Network error: \(underlyingError.localizedDescription)")
showOfflineMessage()
case .requestFailed(let statusCode, let message):
if statusCode == 401 {
print("Session expired - please log in again")
triggerReAuthentication()
} else if statusCode == 404 {
print("Device not found")
} else {
print("Server error \(statusCode): \(message)")
}
case .invalidToken(let message):
print("Invalid token: \(message)")
refreshToken()
case .decodingFailed(let error):
print("Failed to parse response: \(error)")
reportBug()
default:
print("Error: \(error.localizedDescription)")
if let suggestion = error.recoverySuggestion {
print("Suggestion: \(suggestion)")
}
}
}
DeviceError Types
public enum DeviceError: LocalizedError {
case networkError(error: Error)
case requestFailed(statusCode: Int, message: String)
case invalidUrl(url: String)
case decodingFailed(error: Error)
case encodingFailed(message: String)
case invalidResponse(message: String)
case invalidToken(message: String)
case missingConfiguration(message: String)
}
Error Properties
// Each error provides:
error.errorDescription // Main error message
error.failureReason // Why the error occurred
error.recoverySuggestion // How to fix it
© Copyright 2025-2026 Ping Identity Corporation. All Rights Reserved
View on GitHub