mcpField
P
ping-orchestration-android-sdk

ping-orchestration-android-sdk

github.com/pingidentity安全146 次安装13 周前更新
代码仓库

关于 ping-orchestration-android-sdk

用于生成 Ping Orchestration Android SDK 认证集成代码或完整 Android 项目的技能,涵盖 Journey、DaVinci 和 OIDC Web 流程,并提供回调与收集器实现参考。

主要功能

  1. 01生成完整 Android 项目
  2. 02生成 Kotlin 集成文件
  3. 03配置 Journey、DaVinci 和 OIDC Web
  4. 04渲染回调与收集器
  5. 05校验 OAuth 和 Gradle 配置

适用场景

  1. 01创建 Android 认证示例应用
  2. 02为现有应用添加 Ping SDK 认证
  3. 03查阅 SDK 流程与回调指南

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pingidentity/ping-sdk-agent-skills --skill ping-orchestration-android-sdk

安装后在 Claude Code 中输入/ping-orchestration-android-sdk确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
assets/
Theme.kt.template
references/
callback-reference.md
common-mistakes.md

详细介绍

技能参数

不带参数调用时,运行向导。带参数调用时,生成内联内容。

无参数调用 — 向导模式

步骤 W1 — 确定意图,使用 AskUserQuestion:

"What would you like to do?"
Options:
  A) Scaffold a new Android project   — complete Gradle project with Ping SDK wired in
  B) Add to an existing project       — generate only the Kotlin files to drop in
  C) Browse the reference guide       — show the full SDK reference
  D) Something else                   — let me describe what I need
  • A/B → 进入步骤 W2 收集配置,然后生成
  • C → 显示下方集成指南。停止。
  • D → 提出后续自由文本问题,并据此分流。

步骤 W2 — 收集配置(仅限 A 和 B)。

在单个 AskUserQuestion 中询问所有必需参数。显示已有的默认值。所有必填字段都有值之前,不要生成。

参数必填默认值描述
flowType是—davinci · journey · oidc-web
clientId是—OAuth 2.0 客户端 ID
discoveryEndpoint是—完整的 .well-known/openid-configuration URL
redirectUri是com.example.myapp:/oauth2redirectOAuth 2.0 重定向 URI(必须与清单方案匹配)
scopes否openid profile email以空格分隔的 OAuth 2.0 作用域
serverUrl仅限 Journey—PingAM/AIC 基础 URL(末尾不能有 /)
realm仅限 Journeyalpha身份验证域
cookieName仅限 JourneyiPlanetDirectoryProSSO Cookie 名称
journeyName仅限 JourneyLoginJourney 树名称
callbackTierJourney/DaVinci—basic、standard 或 full(参见第 11 节)
packageName是(仅限 A)—Android 包名,例如 com.example.myapp
outputPath是(仅限 A)—项目输出的绝对路径

验证规则:

  • redirectUri 方案必须与 build.gradle.kts 中的 manifestPlaceholders["appRedirectUriScheme"] 匹配。
  • discoveryEndpoint 必须以 /.well-known/openid-configuration 结尾。
  • serverUrl(Journey)必须以 https:// 开头,且末尾不能有 /。
  • scopes 必须包含 openid。如果缺少,请将其添加到开头并发出警告。

步骤 W3 — 确认并生成。 在简短表格中总结各项值,询问“准备好生成了吗——这些信息正确吗?”,然后继续。

步骤 W4 — 生成后检查清单。 在报告完成之前,逐项检查:

  1. Maven 仓库 — settings.gradle.kts 只需要 google() + mavenCentral()。不要添加 maven.forgerock.org(401)或 maven.pingidentity.com。SDK 2.0.0+ 已在 Maven Central 上。

  2. gradle.properties — 必须存在于项目根目录:

    android.useAndroidX=true
    android.enableJetifier=true
    org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
    kotlin.code.style=official
    

    缺少此文件会导致构建因 AndroidX 类路径错误而失败。

  3. gradlew 包装器 — 不要包含在脚手架输出中;告知用户先在 Android Studio 中打开项目。

  4. ExposedDropdownMenuBox 锚点 — 使用 Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),导入 androidx.compose.material3.MenuAnchorType。Material3 1.3.x 中不带参数的重载已弃用。

  5. 版本基线 — agp = "8.9.1"、kotlin = "2.1.21"、compileSdk = 36、targetSdk = 36。AGP 8.9.1 需要 Kotlin 2.1.x(2.0.x 会触发 K2 崩溃 FirIncompatibleClassExpressionChecker)。androidx.browser:1.9.0 和 androidx.core:1.17.0+ 需要 AGP 8.9.1+ 和 compileSdk 36。

  6. minSdk 下限 — Journey:minSdk = 29。DaVinci/OIDC:minSdk = 28。

  7. Ping Identity 品牌标识 — 始终应用。读取 assets/Theme.kt.template,写入 ui/theme/PingTheme.kt,替换 PLACEHOLDER_PACKAGE_NAME。将 assets/ping_logo.png 复制到 app/src/main/res/drawable/。

  8. res/values/themes.xml 父主题 — 使用 Theme.AppCompat.Light.NoActionBar,不要使用 Theme.Material3.DayNight.NoActionBar。Material3 XML 主题需要依赖列表中没有的 com.google.android.material 库。Theme.AppCompat.Light.NoActionBar 始终会通过 activity-compose 作为传递依赖提供。使用错误的父主题会导致构建时 AAPT 资源链接失败。

  9. 应用图标 — 绝不要引用 @mipmap/ic_launcher — 脚手架项目没有 mipmap 目录。在 AndroidManifest.xml 中,对 android:icon 和 android:roundIcon 都使用 @drawable/ping_logo(已在步骤 7 中复制)。引用缺失的 mipmap 会导致 AAPT 资源链接失败,与上述主题错误相同。


Ping Orchestration Android SDK — 集成指南

流程类型

Journey(原生) — 在应用内组合 UI,使用回调,目标为 PingOne AIC / PingAM。 DaVinci — 类似 Journey,但目标为 PingOne DaVinci;使用收集器,不使用 serverUrl/realm/cookie。 OIDC Web — 通过 Custom Tabs 使用浏览器;配置重定向 URI intent filter。

模块映射

Artifact(com.pingidentity.sdks:<name>)添加时机
journeyJourney 流程 + 所有回调类型
davinciDaVinci 流程 + 所有收集器类型
oidc独立的 OidcWebClient
external-idp社交登录(IdpCollector、SelectIdpCallback)
fidoFIDO2 / Passkeys
protectPingOne Protect 风险信号
binding设备绑定与签名
device-profile设备元数据
oathTOTP/HOTP — artifact 是 oath,不是 mfa-oath
push推送 MFA — artifact 是 push,不是 mfa-push
recaptcha-enterprisereCAPTCHA Enterprise

1 — Gradle 依赖项

SDK 2.0.0+ 已发布到 Maven Central — 无需自定义仓库。

// settings.gradle.kts
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
// app/build.gradle.kts
android {
    compileSdk = 36
    defaultConfig {
        minSdk = 29
        targetSdk = 36
        manifestPlaceholders["appRedirectUriScheme"] = "com.example.myapp"
    }
    buildFeatures { buildConfig = true }
}

dependencies {
    implementation("com.pingidentity.sdks:journey:2.0.0")   // or :davinci:2.0.0
    implementation("com.pingidentity.sdks:fido:2.0.0")
    implementation("com.pingidentity.sdks:protect:2.0.0")
    implementation("com.pingidentity.sdks:binding:2.0.0")
    implementation("com.pingidentity.sdks:device-profile:2.0.0")
    implementation("com.pingidentity.sdks:external-idp:2.0.0")

    val composeBom = platform("androidx.compose:compose-bom:<bom_version>")
    implementation(composeBom)
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
    implementation("androidx.navigation:navigation-compose")
}

gradle/libs.versions.toml 基线:agp = "8.9.1"、kotlin = "2.1.21"。

2 — AndroidManifest.xml — 重定向 URI

<activity android:name=".MainActivity" ...>
    <intent-filter android:autoVerify="true">
        <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>

3 — 节点类型(com.pingidentity.orchestrate)

sealed interface Node
// ContinueNode  — more input needed; .next() to advance
// SuccessNode   — authenticated; .session
// ErrorNode     — server error (recoverable); .message: String
// FailureNode   — unexpected error; .cause: Throwable (NON-OPTIONAL)

Node 不实现 Equatable — 在状态中使用 counter: Int 以强制重新组合。

4 — DaVinci 配置

import com.pingidentity.davinci.DaVinci
import com.pingidentity.davinci.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD  // top-level extension — explicit import required

val daVinci = DaVinci {
    logger = Logger.STANDARD
    module(Oidc) {
        clientId          = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes            = mutableSetOf("openid", "profile", "email")
        redirectUri       = BuildConfig.REDIRECT_URI
    }
}

// Start: val node: Node = daVinci.start()          — suspend
// Advance: val next: Node = continueNode.next()    — suspend
// Session: val user = daVinci.user()               — suspend, null if none
// Logout: user?.logout()
// Token: user?.accessToken()  // Result<Token, OidcError>

5 — Journey 配置

import com.pingidentity.journey.Journey
import com.pingidentity.journey.module.Oidc
import com.pingidentity.logger.Logger
import com.pingidentity.logger.STANDARD     // top-level extension
import com.pingidentity.journey.start       // top-level extension — NOT a Journey member
import com.pingidentity.journey.user        // top-level extension — NOT a Journey member

val journey = Journey {
    logger    = Logger.STANDARD
    serverUrl = BuildConfig.SERVER_URL
    realm     = BuildConfig.REALM
    cookie    = BuildConfig.COOKIE_NAME
    module(Oidc) {
        clientId          = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes            = mutableSetOf("openid", "profile", "email")
        redirectUri       = BuildConfig.REDIRECT_URI
    }
}

// Start:   val node: Node = journey.start(BuildConfig.JOURNEY_NAME)  — suspend
// Session: val user = journey.user()   — suspend, null if none
// Logout:  user?.logout()
// Token:   user?.token()  // Result<Token, OidcError>

以下四个符号都是顶层扩展 — 未导入时无法解析:

符号导入
journey.start(name)import com.pingidentity.journey.start
journey.user()import com.pingidentity.journey.user
continueNode.callbacksimport com.pingidentity.journey.plugin.callbacks
Logger.STANDARDimport com.pingidentity.logger.STANDARD

6 — MVVM 状态与 ViewModel

data class AuthState(val node: Node? = null, val counter: Int = 0)

class AuthViewModel : ViewModel() {
    val state = MutableStateFlow(AuthState())
    val loading = MutableStateFlow(false)

    fun start() { viewModelScope.launch {
        loading.value = true
        val node = journey.start(BuildConfig.JOURNEY_NAME)
        state.update { AuthState(node = node, counter = it.counter + 1) }
        loading.value = false
    }}

    fun next(continueNode: ContinueNode) { viewModelScope.launch {
        loading.value = true
        val node = continueNode.next()
        state.update { AuthState(node = node, counter = it.counter + 1) }
        loading.value = false
    }}

    fun refresh() { state.update { it.copy(counter = it.counter + 1) } }

    fun logout(onCompleted: () -> Unit) { viewModelScope.launch {
        journey.user()?.logout()
        state.value = AuthState()
        onCompleted()
    }}
}

7 — 顶层身份验证屏幕

@Composable
fun AuthScreen(viewModel: AuthViewModel, onSuccess: () -> Unit) {
    BackHandler { viewModel.start() }  // restart rather than navigate back
    val state by viewModel.state.collectAsState()
    val loading by viewModel.loading.collectAsState()

    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        when (val node = state.node) {
            is ContinueNode -> CallbackNodeView(node, { viewModel.refresh() }) { viewModel.next(node) }
            is FailureNode  -> PingErrorCard(node.cause.message ?: "Unexpected error") { viewModel.start() }
            is ErrorNode    -> PingErrorCard(node.message) { viewModel.start() }
            is SuccessNode  -> LaunchedEffect(Unit) { onSuccess() }  // LaunchedEffect prevents recomposition loop
            null            -> LaunchedEffect(Unit) { viewModel.start() }
        }
        if (loading) PingLoadingOverlay("Signing in…")
    }
}

8 — DaVinci:收集器渲染

@Composable
fun ContinueNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: (ContinueNode) -> Unit) {
    var hasSubmittable = false
    node.collectors.forEach { collector ->
        when (collector) {
            is TextCollector     -> TextCollectorField(collector, onNodeUpdated)
            is PasswordCollector -> PasswordCollectorField(collector, onNodeUpdated)
            is LabelCollector    -> Text(collector.content, style = MaterialTheme.typography.bodyMedium)
            is SingleSelectCollector -> SingleSelectField(collector, onNodeUpdated)
            is MultiSelectCollector  -> MultiSelectField(collector, onNodeUpdated)
            is PhoneNumberCollector  -> PhoneNumberField(collector, onNodeUpdated)
            is SubmitCollector -> { hasSubmittable = true; SubmitButton(collector) { onNext(node) } }
            is FlowCollector   -> { hasSubmittable = true; FlowButton(collector) { onNext(node) } }
            is DeviceRegistrationCollector   -> { hasSubmittable = true; DeviceRegistrationView(collector) { onNext(node) } }
            is DeviceAuthenticationCollector -> { hasSubmittable = true; DeviceAuthView(collector) { onNext(node) } }
            is FidoRegistrationCollector     -> { hasSubmittable = true; FidoRegistrationView(collector) { onNext(node) } }
            is FidoAuthenticationCollector   -> { hasSubmittable = true; FidoAuthView(collector) { onNext(node) } }
            is ProtectCollector -> ProtectCollectorView(collector) { onNext(node) }
            is IdpCollector     -> IdpButton(collector) { onNext(node) }
        }
    }
    if (!hasSubmittable) { PingPrimaryButton("Next") { onNext(node) } }
}

收集器的关键属性:

收集器关键属性输入
TextCollectorlabel、value、requiredcollector.value = text
PasswordCollectorlabel、value先调用 collector.validate(pwd);再执行 collector.value = pwd
SubmitCollector / FlowCollectorlabel调用 onNext
LabelCollectorcontent仅显示
SingleSelectCollectorlabel、options: List<Option>、value: Stringcollector.value = option.value
MultiSelectCollectorlabel、options: List<Option>、value: List<String>collector.value = listOf(...)
PhoneNumberCollectorlabel、defaultCountryCode、countryCode、phoneNumber设置两个字段
DeviceRegistrationCollector / DeviceAuthenticationCollectordevices: List<Device>执行 collector.value = device,然后调用 onNext
ProtectCollector—执行 suspend collector.collect(),然后调用 onNext
FidoRegistrationCollector—执行 suspend collector.register(),然后调用 onNext
FidoAuthenticationCollector—执行 suspend collector.authenticate(),然后调用 onNext
IdpCollectorlabel、type、iconUrl执行 suspend collector.authorize(redirectUri)

9 — Journey:回调渲染

import com.pingidentity.journey.plugin.callbacks  // extension property — required

@Composable
fun CallbackNodeView(node: ContinueNode, onNodeUpdated: () -> Unit, onNext: () -> Unit) {
    var showNext = true
    node.callbacks.forEach { callback ->
        when (callback) {
            is NameCallback            -> NameField(callback, onNodeUpdated)
            is PasswordCallback        -> PasswordField(callback, onNodeUpdated)
            is ValidatedUsernameCallback -> ValidatedUsernameField(callback, onNodeUpdated)
            is ValidatedPasswordCallback -> ValidatedPasswordField(callback, onNodeUpdated)
            is TextInputCallback       -> TextInputField(callback, onNodeUpdated)
            is TextOutputCallback      -> Text(callback.message, style = MaterialTheme.typography.bodyMedium)
            is ChoiceCallback          -> ChoiceField(callback, onNodeUpdated)
            is ConfirmationCallback    -> { showNext = false; ConfirmationButtons(callback, onNext) }
            is TermsAndConditionsCallback -> TermsCheckbox(callback, onNodeUpdated)
            is KbaCreateCallback       -> KbaField(callback, onNodeUpdated)
            is BooleanAttributeInputCallback -> BooleanAttributeField(callback, onNodeUpdated)
            is StringAttributeInputCallback  -> StringAttributeField(callback, onNodeUpdated)
            is PollingWaitCallback     -> { showNext = false; PollingWaitView(callback, onNext) }
            is DeviceProfileCallback   -> { showNext = false; DeviceProfileView(callback, onNext) }
            is DeviceBindingCallback   -> { showNext = false; DeviceBindingView(callback, onNext) }
            is FidoRegistrationCallback   -> { showNext = false; FidoRegistrationView(callback, onNext) }
            is FidoAuthenticationCallback -> { showNext = false; FidoAuthView(callback, onNext) }
            is PingOneProtectInitializeCallback -> { showNext = false; ProtectInitView(callback, onNext) }
            is PingOneProtectEvaluationCallback -> { showNext = false; ProtectEvalView(callback, onNext) }
            is SelectIdpCallback -> { showNext = false; SelectIdpView(callback, onNext) }
        }
    }
    if (showNext) { PingPrimaryButton("Next", onClick = onNext) }
}

回调参考(准确的名称、包名、属性名和挂起方法 — 猜错会导致编译错误):references/callback-reference.md

关键注意事项:ValidatedUsernameCallback → .username(不是 .value);TextInputCallback → .text;ChoiceCallback → .selectedIndex;TermsAndConditionsCallback → .accepted;DeviceProfileCallback.collect()、DeviceBindingCallback.bind()、PingOneProtectInitializeCallback.start()。

自动推进的回调 — 在 LaunchedEffect(callback) 中包装,并设置 showNext = false:PollingWaitCallback、DeviceProfileCallback、DeviceBindingCallback、FidoRegistrationCallback、FidoAuthenticationCallback、PingOneProtectInitializeCallback、PingOneProtectEvaluationCallback。

10 — OIDC Web 流程

import com.pingidentity.oidc.OidcWebClient
import com.pingidentity.oidc.module.Oidc

val webClient = OidcWebClient {
    module(Oidc) {
        clientId = BuildConfig.CLIENT_ID
        discoveryEndpoint = BuildConfig.DISCOVERY_ENDPOINT
        scopes = mutableSetOf("openid", "profile", "email")
        redirectUri = BuildConfig.REDIRECT_URI
    }
}

// In ViewModel:
webClient.authorize()
    .onSuccess { user -> /* authenticated */ }
    .onFailure { error -> /* handle OidcError */ }

val user = webClient.user()  // null if no active session

authorize() 是挂起函数。它会启动 Custom Tabs,等待重定向,并将代码交换为令牌。

11 — Collector/Callback 层级

层级Journey 新增项DaVinci 新增项额外模块
基础NameCallback、PasswordCallback、TextOutputCallback、ChoiceCallbackTextCollector、PasswordCollector、SubmitCollector、LabelCollector、FlowCollector无
标准+ 已验证的回调、属性回调、ConfirmationCallback、TermsAndConditionsCallback、KbaCreateCallback、PollingWaitCallback,以及所有设备/FIDO/Protect/SelectIdp 回调+ SingleSelectCollector、MultiSelectCollector、PhoneNumberCollector、设备/FIDO/Protect collectorsfido、protect、binding、device-profile、external-idp
完整+ IdpCallback、ReCaptchaEnterpriseCallback+ IdpCollector+ recaptcha-enterprise
FIDO / Protect / device binding?  No → Basic
  Yes → Social login or reCAPTCHA?  No → Standard  Yes → Full

12 — 常见错误

请参阅 references/common-mistakes.md — 涵盖导入错误、回调属性名称错误、挂起方法名称错误、重组循环、Gradle/AGP 版本问题、AAPT 错误,以及品牌组件相关问题。

13 — MFA 模块

推送(FCM) — artifact com.pingidentity.sdks:push:

val pushClient = PushClient { }
pushClient.addCredentialFromUri(uri)           // QR scan
pushClient.processNotification(remoteMessage.data)  // FCM
pushClient.approveNotification(notificationId)

OATH(TOTP/HOTP) — artifact com.pingidentity.sdks:oath:

val oathClient = OathClient { }
oathClient.addCredentialFromUri(otpauthUri)
val code = oathClient.generateCode(credentialId)

Protect(独立):

Protect.config { isBehavioralDataCollection = true; envId = "api.pingone.com" }
Protect.init()

14 — ForgeRock → Ping 迁移

旧版Ping SDK
org.forgerock:forgerock-authcom.pingidentity.sdks:journey
FRAuth.start()journey.start("Login")
FRUser.getCurrentUser()journey.user() / daVinci.user()
Node.getCallbacks()continueNode.callbacks

如需完整的迁移指南,请阅读 forgerock-to-ping-journey-migration skill。

15 — Ping Identity 视觉品牌设计

将 assets/Theme.kt.template 写入 ui/theme/PingTheme.kt(替换 PLACEHOLDER_PACKAGE_NAME)。将 assets/ping_logo.png 复制到 app/src/main/res/drawable/ping_logo.png。

品牌组件:PingPrimaryButton、PingHeaderView、PingTextFieldBranded(此名称可避免与 PingTextField 颜色常量冲突)、PingSecureField、PingErrorMessage、PingErrorCard、PingLoadingOverlay(必须是 Box(Modifier.fillMaxSize()) 中最上层的同级元素)。

登录屏幕:使用 Box(fillMaxSize) 包裹一个 Column(verticalScroll),顶部放置 PingHeaderView,下方放置 CallbackNodeView/CollectorNodeView,加载时叠加显示 PingLoadingOverlay。为 Checkbox/Switch 应用 PingRed 色调。

评论

支持 Markdown