Use when building Android authentication with Ping Identity — scaffolds a complete Jetpack Compose + MVVM authentication flow using the Ping Orchestration Android SDK against PingOne Advanced Identity Cloud (AIC) or PingAM. Handles Journey configuration, OIDC token exchange, dynamic callback rendering, device binding, FIDO2, and logout.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ping-orchestration-sdks:ping-orchestration-android-journey-sdkThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Scaffold a complete authentication flow in an Android app using Jetpack Compose, MVVM, and the Ping Orchestration Android SDK (Journey module).
assets/AuthScreen.kt.templateassets/AuthState.kt.templateassets/AuthViewModel.kt.templateassets/CallbackFields.kt.templateassets/CallbackNode.kt.templateassets/DeviceBindingCallbackField.kt.templateassets/DeviceBindingCallbackViewModel.kt.templateassets/DeviceSigningVerifierCallbackField.kt.templateassets/DeviceSigningVerifierCallbackViewModel.kt.templateassets/JourneyConfig.kt.templateassets/build.gradle.kts.templatereferences/callbacks.mdreferences/journey-sdk.mdreferences/oidc-config.mdscripts/scaffold_auth.shScaffold a complete authentication flow in an Android app using Jetpack Compose, MVVM, and the Ping Orchestration Android SDK (Journey module).
| Field | Value |
|---|---|
| Language | Kotlin |
| Framework | Android, Jetpack Compose, AndroidX ViewModel |
| SDK | Ping Orchestration Android SDK (Journey module, com.pingidentity.sdks:journey) |
| Pattern | MVVM (Model-View-ViewModel) |
| Min SDK | 29 |
| Compile SDK | 36 |
This skill adds a complete authentication flow to an Android application using Jetpack Compose and the MVVM pattern. It uses the Ping Orchestration Android SDK (Journey module) to authenticate users against PingOne AIC (Advanced Identity Cloud).
The implementation covers:
StateFlowAdd the following to the app-level build.gradle.kts. See build.gradle.kts template for a complete example.
dependencies {
// Ping Orchestration Android SDK (Journey module)
implementation("com.pingidentity.sdks:journey:<version>")
// Jetpack Compose BOM
val composeBom = platform("androidx.compose:compose-bom:<bom_version>")
implementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.activity:activity-compose")
implementation("androidx.navigation:navigation-compose")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
// DataStore (for token/session persistence)
implementation("androidx.datastore:datastore-preferences")
}
Add the redirect URI scheme as an intent filter (required for OAuth 2.0 redirect):
<activity android:name=".MainActivity" ...>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${appRedirectUriScheme}" />
</intent-filter>
</activity>
In build.gradle.kts defaultConfig:
manifestPlaceholders["appRedirectUriScheme"] = "org.forgerock.demo"
See OIDC Configuration Reference for full redirect URI setup details.
Agent instruction: Before generating any file, collect the values below from the user. Ask for all
requiredparameters up front in a single prompt. For optional parameters, show the default and ask whether the user wants to override it. Do not proceed to Step 1 until every required parameter has a non-empty value.
| Parameter | Required | Default | Description |
|---|---|---|---|
serverUrl | Yes | — | Base URL of the PingOne AIC tenant (e.g. https://your-tenant.forgeblocks.com/am) |
realm | Yes | alpha | Realm name inside the tenant |
clientId | Yes | — | OAuth 2.0 Client ID registered in PingOne AIC for this Android app |
discoveryEndpoint | Yes | — | Full OIDC discovery endpoint URL (.well-known/openid-configuration) |
scopes | Yes | openid email profile phone | Space-separated OAuth 2.0 scopes to request |
redirectUri | Yes | org.forgerock.demo:/oauth2redirect | OAuth 2.0 redirect URI registered in PingOne AIC |
cookieName | Yes | iPlanetDirectoryPro | SSO cookie name for the realm; omit the cookie line if blank |
journeyName | No | Login | Name of the Journey/Tree to invoke on start |
Before I generate the files, I need a few details about your PingOne AIC setup:
1. Server URL — What is your PingOne AIC tenant base URL?
e.g. https://your-tenant.forgeblocks.com/am
2. Client ID — What is the OAuth 2.0 Client ID for this Android app?
3. Discovery Endpoint — What is the full OIDC discovery endpoint URL?
e.g. https://your-tenant.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration
4. Realm — Which realm? (default: alpha)
5. Scopes — Which OAuth 2.0 scopes? (default: openid email profile phone)
6. Redirect URI — What is the redirect URI? (default: org.forgerock.demo:/oauth2redirect)
7. Cookie name — SSO cookie name? (default: iPlanetDirectoryPro)
8. Journey name — Which Journey/Tree to start? (default: Login)
serverUrl must start with https:// and not end with a trailing /.discoveryEndpoint must end with /.well-known/openid-configuration.clientId must be non-empty.redirectUri must follow <scheme>:/<path> format; the scheme must match manifestPlaceholders["appRedirectUriScheme"].scopes does not include openid, prepend it automatically and warn the user.discoveryEndpoint domain differs from serverUrl domain, warn the user but proceed.Create JourneyConfig.kt. See JourneyConfig.kt template.
Agent instruction: Substitute all
<parameter>placeholders with values collected above. Omit thecookieline entirely ifcookieNameis blank.
val journey = Journey {
logger = Logger.STANDARD
serverUrl = "<serverUrl>"
realm = "<realm>"
cookie = "<cookieName>"
module(Oidc) {
clientId = "<clientId>"
discoveryEndpoint = "<discoveryEndpoint>"
scopes = mutableSetOf("openid", "email", "profile", "phone")
redirectUri = "<redirectUri>"
}
}
See Journey SDK API Reference for all configuration options.
Create AuthState.kt. See AuthState.kt template.
data class AuthState(
val node: Node? = null,
val counter: Int = 0 // Triggers recomposition when node is the same object
)
Create AuthViewModel.kt. See AuthViewModel.kt template.
Key methods:
start() — Starts or restarts the Journeynext(node: ContinueNode) — Advances to the next node after callbacks are populatedrefresh() — Triggers recomposition without advancinglogout(onCompleted) — Logs out the current userCreate CallbackNode.kt which dispatches each callback in a ContinueNode to its dedicated Composable. See CallbackNode.kt template.
The pattern:
@Composable
fun CallbackNode(continueNode: ContinueNode, onNodeUpdated: () -> Unit, onNext: () -> Unit) {
var showNext = true
continueNode.callbacks.forEach { callback ->
when (callback) {
is NameCallback -> NameCallbackField(callback, onNodeUpdated)
is PasswordCallback -> PasswordCallbackField(callback, onNodeUpdated)
is ChoiceCallback -> ChoiceCallbackField(callback, onNodeUpdated)
is ConfirmationCallback -> { showNext = false; ConfirmationCallbackField(callback, onNext) }
// Add more callbacks as needed
}
}
if (showNext) { Button(onClick = onNext) { Text("Next") } }
}
See Callback Types Reference for the full list of supported callbacks.
Create CallbackFields.kt with individual Composables for each callback type. See CallbackFields.kt template.
All callback types supported by the Ping Android SDK:
Core Callbacks (Journey module):
| Callback Class | Composable | Description |
|---|---|---|
NameCallback | NameCallbackField | Text input for username |
PasswordCallback | PasswordCallbackField | Secure text input with visibility toggle |
ValidatedUsernameCallback | ValidatedUsernameCallbackField | Username with policy validation |
ValidatedPasswordCallback | ValidatedPasswordCallbackField | Password with policy validation |
TextInputCallback | TextInputCallbackField | Generic text input (e.g., OTP) |
TextOutputCallback | TextOutputCallbackField | Display messages (info, warning, error) |
SuspendedTextOutputCallback | SuspendedTextOutputCallbackField | Suspended journey (magic link) |
BooleanAttributeInputCallback | BooleanCallbackField | Switch/checkbox for boolean attributes |
NumberAttributeInputCallback | NumberCallbackField | Numeric input |
StringAttributeInputCallback | StringAttributeCallbackField | String attribute (email, name) |
ChoiceCallback | ChoiceCallbackField | Dropdown for multiple choice selection |
ConfirmationCallback | ConfirmationCallbackField | Action buttons (Yes/No, OK/Cancel) |
KbaCreateCallback | KbaCreateCallbackField | Security question and answer |
TermsAndConditionsCallback | TermsCallbackField | Terms acceptance checkbox |
ConsentMappingCallback | ConsentCallbackField | Consent to share profile data |
PollingWaitCallback | PollingWaitCallbackField | Auto-advancing wait step |
HiddenValueCallback | — | Non-visual — hidden form value |
MetadataCallback | — | Non-visual — specializes into FIDO2/Protect callbacks |
Optional Callbacks (additional modules):
| Callback Class | Module | Composable | Description |
|---|---|---|---|
SelectIdpCallback | external-idp | SelectIdpCallbackField | Social/external IdP selection |
IdpCallback | external-idp | IdpCallbackField | External IdP OAuth flow |
DeviceProfileCallback | device-profile | DeviceProfileCallbackField | Auto-collects device metadata |
DeviceBindingCallback | binding | DeviceBindingCallbackField | Binds device to user account |
DeviceSigningVerifierCallback | binding | DeviceSigningVerifierCallbackField | Signs challenge with device key |
FidoRegistrationCallback | fido | FidoRegistrationCallbackField | FIDO2/Passkey registration |
FidoAuthenticationCallback | fido | FidoAuthenticationCallbackField | FIDO2/Passkey authentication |
PingOneProtectInitializeCallback | protect | PingOneProtectInitializeCallbackField | Initializes PingOne Protect |
PingOneProtectEvaluationCallback | protect | PingOneProtectEvaluationCallbackField | Evaluates threat signals |
ReCaptchaEnterpriseCallback | recaptcha-enterprise | ReCaptchaEnterpriseCallbackField | reCAPTCHA Enterprise verification |
Each callback Composable follows this pattern:
callback.prompt)callback.name = text)onNodeUpdated() or onNext() as appropriateNote: Auto-advancing callbacks (
PollingWaitCallback,DeviceProfileCallback,DeviceBindingCallback,DeviceSigningVerifierCallback,FidoRegistrationCallback,FidoAuthenticationCallback,PingOneProtectInitializeCallback,PingOneProtectEvaluationCallback,ReCaptchaEnterpriseCallback) perform their operation inLaunchedEffectand callonNext()automatically when complete. SetshowNext = falsefor these.
See also:
Create AuthScreen.kt. See AuthScreen.kt template.
@Composable
fun AuthScreen(viewModel: AuthViewModel, onSuccess: () -> Unit) {
BackHandler { viewModel.start() }
val state by viewModel.state.collectAsState()
val loading by viewModel.loading.collectAsState()
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
if (loading) CircularProgressIndicator()
when (val node = state.node) {
is ContinueNode -> CallbackNode(node, onNodeUpdated = { viewModel.refresh() }, onNext = { viewModel.next(node) })
is FailureNode -> ErrorCard(message = node.cause.message ?: "Unknown error")
is ErrorNode -> ErrorCard(message = node.message)
is SuccessNode -> LaunchedEffect(true) { onSuccess() }
null -> {}
}
}
}
In your NavHost, add:
composable("auth/{journeyName}", arguments = listOf(
navArgument("journeyName") { type = NavType.StringType }
)) { backStack ->
val name = backStack.arguments?.getString("journeyName") ?: "Login"
val vm: AuthViewModel = viewModel(factory = AuthViewModel.factory(name))
AuthScreen(viewModel = vm) {
navController.navigate("home") {
popUpTo("auth/{journeyName}") { inclusive = true }
}
}
}
For quick setup, use the scaffolding script to copy all template files into your project:
chmod +x scripts/scaffold_auth.sh
./scripts/scaffold_auth.sh \
--package com.example.myapp \
--src-dir app/src/main/java \
--journey Login
See scaffold_auth.sh for details.
| Mistake | Fix |
|---|---|
| Wrong redirect URI scheme | Must match manifestPlaceholders["appRedirectUriScheme"] in build.gradle.kts |
Missing openid scope | Always include openid — required for OIDC token exchange |
| Unhandled callback type | Add a when branch in CallbackNode for every callback your Journey uses |
| Hardcoded credentials | Use BuildConfig fields or a config file — never commit secrets |
Forgetting LaunchedEffect for SuccessNode | Navigate away inside LaunchedEffect(true) to avoid recomposition loops |
Not setting showNext = false for ConfirmationCallback | ConfirmationCallback provides its own buttons — hide the default Next button |
Not setting showNext = false for auto-advancing callbacks | Auto-advancing callbacks (DeviceBinding, FIDO, Protect, reCAPTCHA, DeviceProfile, PollingWait) must set showNext = false |
Calling node.next() without populating callbacks | Always set callback values (e.g., callback.name = "...") before advancing |
| Forgetting to register optional callback modules | Import and add the relevant Gradle dependency (binding, fido, protect, external-idp, device-profile, recaptcha-enterprise) for optional callbacks |
Not calling onNext() after auto-advancing callbacks | Auto-advancing callbacks must call onNext() after their async operation completes (use LaunchedEffect) |
ping-quickstart — Platform detection and Ping Identity orientationCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub pingidentity/ping-sdk-agent-skills --plugin ping-orchestration-sdks