Journey: Authentication and Authorization SDK
Overview
Journey is a robust and adaptable library designed to streamline Authentication and Authorization processes within your iOS applications. Engineered for ease of integration and extensibility, it offers a straightforward API to manage authentication flows and handle the various states encountered during this process.
sequenceDiagram
Developer ->> Journey: Create Journey instance
Journey ->> Developer: Journey
Developer ->> Journey: start(Journey Name)
Journey ->> AIC: /authenticate
AIC ->> AIC: Launch Journey Flow
AIC ->> Journey: Callback(s)
Journey ->> Developer: Node
Developer ->> Developer: Gather credentials
Developer ->> Journey: next()
Journey ->> AIC: /authenticate
AIC ->> Journey: Session Token
Journey ->> Developer: Session Token
opt OIDC Module
Journey ->> AIC: /authorize
AIC ->> Journey: Authorization Code
Journey ->> AIC: /token
AIC ->> Journey: Access Token
Journey ->> Journey: persist access token
Journey ->> Developer: Access Token
end
For a deeper understanding of PingOne AIC Journeys, refer to the official documentation available here.
Installation
Add dependency to your project
To integrate Journey into your iOS project, add the following dependency to your
Podfile or Package.swift file:
CocoaPods
Add to your Podfile:
pod 'PingJourney', '~> 2.0.0'
Then run:
pod install
Swift Package Manager
Add the following dependency to your Package.swift file:
.package(url: "https://github.com/ForgeRock/ping-ios-sdk.git", from: "2.0.0")
Then add PingJourney to your target’s dependencies:
.target(
name: "YourTarget",
dependencies: [
.product(name: "PingJourney", package: "ping-ios-sdk")
]
)
Alternatively, in Xcode:
- Go to File > Add Package Dependencies…
- Enter the repository URL:
https://github.com/ForgeRock/ping-ios-sdk.git - Select version 2.0.0 or later
- Add the
PingJourneylibrary to your target
Note: PingJourney depends on PingOrchestrate which will be automatically installed.
Getting Started
Basic Usage
To begin using the Journey class, instantiate it within your code, providing a configuration block
to customize its
behavior. This configuration allows you to set parameters such as network timeout and logging
preferences.
Here’s a basic example of creating a Journey instance and initiating the authentication flow:
let journey = Journey.createJourney { config in
config.serverUrl = "https://openam-sdks.forgeblocks.com/am"
}
var node = journey.start("login") // Start the login authentication flow. Pass the Journey name
if let current = node as? ContinueNode {
let next = await current.next() // Continue to the next step
}
Integrating the OIDC Module
Optionally, you can enhance the Journey instance with the oidc module. This module enables the
discovery of OpenID
Connect (OIDC) endpoints using the discoveryEndpoint attribute.
let journey = Journey.createJourney { config in
// Oidc as module
config.module(PingJourney.OidcModule.config) { oidcValue in
oidcValue.clientId = "test"
oidcValue.discoveryEndpoint = "https://your_openam_domain/am/oauth2/alpha/.well-known/openid-configuration"
oidcValue.scopes = ["openid", "email", "address"]
oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
// Add other OIDC configurations as needed
}
}
Advanced Journey Configuration
The Journey configuration block offers further customization options:
let journey = Journey.createJourney { config in
config.timeout = 30 // Network request timeout in seconds (default: 30s)
config.logger = LogManager.standard // Use the standard logger for output
config.realm = "<realm_name>" // Specify the realm for authentication
config.cookie = "<cookie_name>" // Specify the cookie name for session management
config.module(PingJourney.OidcModule.config) { oidcValue in
oidcValue.clientId = "test"
oidcValue.discoveryEndpoint = "https://your_openam_domain/am/oauth2/alpha/.well-known/openid-configuration"
oidcValue.scopes = ["openid", "email", "address"]
oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect"
// Add other OIDC configurations as needed
}
}
Navigating the Authentication Flow
The start() method initiates the authentication journey and returns a Node instance,
representing the current state
of the process. You can then use the next() method (available on ContinueNode) to transition to
the subsequent
state.
The resume(uri) method enables the continuation of an authentication flow after you’ve obtained a
resume URI, for
instance, through an Email Suspend Node.
Optionally, you can provide options when starting the journey, such as forceAuth and noSession
let journey = Journey.createJourney { config in
config.serverUrl = "https://openam-sdks.forgeblocks.com/am"
config.realm = "alpha"
config.cookie = "386c0d288Bac4b9"
}
let node = await journey.start("Login") { options in
options.forceAuth = false
options.noSession = false
} // Initiate the authentication journey
// Determine the type of the current Node
switch node {
case let continueNode as ContinueNode:
// Proceed to the next step in the authentication journey
let nextNode = continueNode.next()
case let errorNode as ErrorNode:
// Handle server-side errors (e.g., invalid credentials)
let errorMessage = errorNode.message
let input = errorNode.input // Access the raw JSON response with the input attribute
// Display error to the user
case let failureNode as FailureNode:
// Handle unexpected errors (e.g., network issues, unexpected errors like parsing response)
let errorCause = failureNode.cause
// Log the error and potentially display a generic message
case let successNode as SuccessNode:
// Authentication successful, retrieve the session
let session = successNode.session
let input = successNode.input // Access the raw JSON response with the input attribute
// Proceed with post-authentication actions
default:
break
}
The Node class represents various states within the authentication journey:
| Node Type | Description |
|---|---|
| ContinueNode | Indicates a step in the middle of the authentication journey. Call node.next() to advance to the next node. |
| ErrorNode | Represents a bad request from the server, such as invalid credentials (password, OTP, username). Access the error message using node.message. |
| FailureNode | Signifies an unexpected error during the process, like network connectivity issues. Retrieve the underlying cause using node.cause. |
| SuccessNode | Indicates successful authentication. Obtain the user session details using node.session. |
Providing User Input
When the current Node is a ContinueNode, it often contains a list of callbacks that require
user input. You can
access these callbacks using node.callbacks() and provide the necessary information to each
relevant callback.
The following Callbacks will be supported in the Core Journey Module:
| Callback Name | Callback Description |
|---|---|
| BooleanAttributeInputCallback | Collects true or false. |
| ChoiceCallback | Collects single user input from available choices, retrieves selected choice from user interaction. |
| ConfirmationCallback | Retrieve a selected option from a list of options. |
| ConsentMappingCallback | Prompts the user to consent to share their profile data. |
| HiddenValueCallback | Returns form values that are not visually rendered to the end user. |
| KbaCreateCallback | Collects knowledge-based answers. For example, the name of your first pet. |
| MetadataCallback | Injects key-value metadata into the authentication process. |
| NameCallback | Collects a username. |
| NumberAttributeInputCallback | Collects a number. |
| PasswordCallback | Collects a password or one-time pass code. |
| PollingWaitCallback | Instructs the client to wait for the given period and resubmit the request. |
| StringAttributeInputCallback | Collects the values of attributes for use elsewhere in a tree. |
| SuspendedTextOutputCallback | Pause and resume authentication, sometimes known as “magic links”. |
| TextInputCallback | Collects text input from the end user. For example, a nickname for their account. |
| TextOutputCallback | Provides a message to be displayed to a user with a given message type. |
| TermsAndConditionsCallback | Collects a user’s acceptance of the configured Terms & Conditions. |
| ValidatedCreatePasswordCallback | Collects a password value with optional password policy validation. |
| ValidatedCreateUsernameCallback | Collects a username value with optional username policy validation. |
Here’s how to access and populate the callbacks:
if let current = node as? ContinueNode {
current.callbacks.forEach { callback in
switch callback {
case let nameCallback as NameCallback:
nameCallback.name = "Your Username"
case let passwordCallback as PasswordCallback:
passwordCallback.password = "Your Password"
// Handle other callback types as they are introduced
default:
break
}
}
}
// Proceed to the next Node with the provided input
let nextNode = current.next()
Each specific callback type provides its own properties for accessing labels and setting values.
NameCallback
let prompt = (callback as? NameCallback)?.prompt // Access the prompt/label
(callback as? NameCallback)?.name = "Your Input" // Set the user's input
PasswordCallback
PasswordCallback offers similar properties to NameCallback:
let prompt = (callback as? PasswordCallback)?.prompt // Access the prompt/label
(callback as? PasswordCallback)?.password = "Your Secret" // Set the user's password
Handling Errors
The Journey SDK distinguishes between FailureNode and ErrorNode for different types of errors
encountered during the
authentication flow.
A FailureNode indicates an unexpected issue that prevents the journey from continuing. This could
stem from network
problems, data parsing errors, or internal SDK issues. In such cases, you can access the underlying
Error that
caused the error using node.cause. It’s generally recommended to display a user-friendly generic
error message and
log the details for support investigation.
An ErrorNode, on the other hand, signifies an error response from the authentication server (
typically an HTTP 4xx or
5xx status code). These errors often relate to invalid user input or issues on the server side. You
can retrieve the
specific error message provided by the server using node.message and access the raw JSON
response via node.input.
let node = journey.start() // Initiate the authentication flow
switch node {
case let continueNode as ContinueNode:
// ...
break
case let errorNode as ErrorNode:
let errorMessage = errorNode.message // Retrieve the server-provided error message
let rawResponse = errorNode.input // Access the raw JSON error response
// Display the specific error message to the user
case let failureNode as FailureNode:
let errorCause = failureNode.cause // Retrieve the underlying error
// Log the errorCause for debugging
// Display a generic error message to the user
case let successNode as SuccessNode:
// ...
break
default:
break
}
Integration with SwiftUI
For applications using SwiftUI, you can seamlessly integrate the Journey SDK using an
ObservableObject to manage the
UI state.
ViewModel
@MainActor
class JourneyViewModel: ObservableObject {
/// Published property that holds the current state node data.
@Published public var state: JourneyState = JourneyState()
/// Published property to track whether the view is currently loading.
@Published public var isLoading: Bool = false
var journey: Journey
/// Initializes the view model and starts the Journey orchestration process.
init() {
Task {
await startJourney()
}
}
public func initializeJourney() {
journey = Journey.createJourney { config in
let currentConfig = ConfigurationManager.shared.currentConfigurationViewModel
config.serverUrl = currentConfig?.serverUrl
config.realm = currentConfig?.realm ?? "root"
config.cookie = currentConfig?.cookieName ?? ""
config.module(PingJourney.OidcModule.config) { oidcValue in
oidcValue.clientId = currentConfig?.clientId ?? ""
oidcValue.scopes = Set<String>(currentConfig?.scopes ?? [])
oidcValue.redirectUri = currentConfig?.redirectUri ?? ""
oidcValue.discoveryEndpoint = currentConfig?.discoveryEndpoint ?? ""
}
}
}
/// Starts the Journey orchestration process.
/// - Sets the initial node and updates the `data` property with the starting node.
public func startJourney() async {
await MainActor.run {
isLoading = true
}
let next = await journey.start("Login") { options in
options.forceAuth = false
options.noSession = false
}
await MainActor.run {
self.state = JourneyState(node: next)
isLoading = false
}
}
/// Advances to the next node in the orchestration process.
/// - Parameter node: The current node to progress from.
public func next(node: Node) async {
await MainActor.run {
isLoading = true
}
if let current = node as? ContinueNode {
// Retrieves the next node in the flow.
let next = await current.next()
await MainActor.run {
self.state = JourneyState(node: next)
isLoading = false
}
}
}
public func refresh() {
state = JourneyState(node: state.node)
}
}
View (SwiftUI)
import SwiftUI
struct JourneyView: View {
/// The view model that manages the Davinci flow logic.
@StateObject private var journeyViewModel = JourneyViewModel()
/// A binding to the navigation stack path.
@Binding var path: [String]
var body: some View {
ZStack {
ScrollView {
VStack {
Spacer()
// Handle different types of nodes in the Journey.
switch journeyViewModel.state.node {
case let continueNode as ContinueNode:
// Display the callback view for the next node.
CallbackView(journeyViewModel: journeyViewModel, node: continueNode)
case let errorNode as ErrorNode:
// Handle server-side errors (e.g., invalid credentials)
// Display error to the user
ErrorNodeView(node: errorNode)
if let nextNode = errorNode.continueNode {
CallbackView(journeyViewModel: journeyViewModel, node: nextNode)
}
case let failureNode as FailureNode:
ErrorView(message: failureNode.cause.localizedDescription)
case is SuccessNode:
// Authentication successful, retrieve the session
VStack{}.onAppear {
path.removeLast()
path.append("Token")
}
default:
EmptyView()
}
}
}
}
}
}
Post Authentication Operations
After successful authentication using the Oidc module, the user’s session information is typically
stored securely.
The Journey instance provides methods to interact with this stored session.
// Retrieve the existing user session. If an active session cookie exists, 'user' will not be nil.
// Note: Even if a user object is retrieved, the access and refresh tokens within might be expired.
func accessToken() async {
let token: Result<Token, OidcError>?
let journeyUser = await journey.journeyUser()
token = journeyUser.token()
switch token {
case .success(let token):
await MainActor.run {
self.token = String(describing: token)
let accessToken = token.accessToken
}
LogManager.standard.i("AccessToken: \(self.token.accessToken)")
case .failure(let error):
await MainActor.run {
self.token = "Error: \(error.localizedDescription)"
}
LogManager.standard.e("", error: error)
case .none:
break
}
}
journeyUser.revoke() // Revoke the current access and refresh tokens
journeyUser.userinfo() // Fetch user information using the access token (if valid)
journeyUser.logout() // Initiate the logout process, potentially clearing local session data
Journey Module Dependencies
The Journey module is composed of several foundational components, each responsible for a specific aspect of the SDK’s functionality:
C4Context
title Journey Component Diagram
Component(journey, "Journey", "Library", "Main Journey Library")
Component(logger, "Logger", "Library", "Logging Utility")
Component(plugin, "Plugin", "Library", "Callback & Extension Support")
Component(orchestrator, "Orchestrator", "Library", "Orchestration Logic")
Component(storage, "Storage", "Library", "Data Storage")
Component(platform, "Platform", "Library", "Platform Abstraction (iOS)")
Component(utils, "Utils", "Library", "Utility Functions")
Rel(journey, logger, "Depends on")
Rel(journey, plugin, "Depends on")
Rel(journey, orchestrator, "Depends on")
Rel(journey, storage, "Depends on")
Rel(journey, platform, "Depends on")
Rel(journey, utils, "Depends on")
Journey’s Callback Customization & Extension
Callbacks below will be supported by other modules:
| Callback Name | Callback Description |
|---|---|
| AppIntegrity | Collects a generated token from the client to verify the integrity of the app |
| DeviceBinding | Cryptographically bind a mobile device to a user account. |
| DeviceProfileCallback | Collects meta and/or location data about the authenticating device. |
| DeviceSigningVerifier | Verify ownership of a bound device by signing a challenge. |
| PingOneProtectEvaluationCallback | Collects captured contextual data from the client to perform risk evaluations. |
| PingOneProtectInitializeCallback | Instructs the client to start capturing contextual data for risk evaluations |
| ReCaptchaCallback | Provides data required to use a CAPTCHA in your apps. |
| ReCaptchaEnterpriseCallback | Provides data required to use reCAPTCHA Enterprise in your apps. |
| WebAuthnRegistrationCallback | WebAuthn Registration. |
| WebAuthnAuthenticationCallback | WebAuthn Authentication. |
| SelectIdpCallback | External Identity provider selection. |
| IdpCallback | External Identity provider authentication. |
© Copyright 2025-2026 Ping Identity Corporation. All Rights Reserved
View on GitHub