ping-orchestration-android-sdk について
Ping Orchestration Android SDKを使った認証統合コードやAndroidプロジェクトを生成するスキルです。Journey、DaVinci、OIDC Webの設定と、コールバックやコレクターの実装を案内します。
主な機能
- 01Androidプロジェクト一式の生成
- 02Kotlin統合ファイルの生成
- 03Journey、DaVinci、OIDC Webの設定
- 04コールバックとコレクターの表示
- 05OAuthとGradle設定の検証
活用シーン
- 01Android認証サンプルの作成
- 02既存アプリへのPing SDK認証の追加
- 03SDKフローとコールバックの参照
インストール
コマンドをコピーしてターミナルで実行します。
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.mdassets/Theme.kt.templatereferences/callback-reference.mdcommon-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 のみ)。
必要なパラメーターをすべて 1 つの AskUserQuestion で質問します。デフォルト値がある場合は表示します。必須フィールドがすべて入力されるまで、生成を開始しないでください。
| パラメーター | 必須 | デフォルト | 説明 |
|---|---|---|---|
flowType | はい | — | davinci · journey · oidc-web |
clientId | はい | — | OAuth 2.0 クライアント ID |
discoveryEndpoint | はい | — | .well-known/openid-configuration の完全な URL |
redirectUri | はい | com.example.myapp:/oauth2redirect | OAuth 2.0 リダイレクト URI(マニフェストのスキームと一致している必要があります) |
scopes | いいえ | openid profile email | スペース区切りの OAuth 2.0 スコープ |
serverUrl | Journey のみ | — | PingAM/AIC のベース URL(末尾に / を付けないでください) |
realm | Journey のみ | alpha | 認証レルム |
cookieName | Journey のみ | iPlanetDirectoryPro | SSO Cookie 名 |
journeyName | Journey のみ | Login | Journey ツリー名 |
callbackTier | Journey/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 — 生成後のチェックリスト。完了を報告する前に、以下を確認してください。
-
Maven リポジトリ —
settings.gradle.ktsに必要なのはgoogle()とmavenCentral()のみです。maven.forgerock.org(401)やmaven.pingidentity.comは追加しないでください。SDK 2.0.0 以降は Maven Central にあります。 -
gradle.properties— プロジェクトのルートに存在する必要があります。android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 kotlin.code.style=officialこれがないと、AndroidX クラスパスの問題によりビルドに失敗します。
-
gradlewラッパー — 雛形として出力するファイルには含めないでください。まず Android Studio で開くようユーザーに伝えてください。 -
ExposedDropdownMenuBoxのアンカー —Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)を使用し、androidx.compose.material3.MenuAnchorTypeをインポートします。引数なしのオーバーロードは Material3 1.3.x で非推奨です。 -
バージョンの基準 —
agp = "8.9.1"、kotlin = "2.1.21"、compileSdk = 36、targetSdk = 36。AGP 8.9.1 には Kotlin 2.1.x が必要です(2.0.x ではFirIncompatibleClassExpressionCheckerによる K2 クラッシュが発生します)。androidx.browser:1.9.0とandroidx.core:1.17.0+には AGP 8.9.1 以降とcompileSdk 36が必要です。 -
minSdkの下限 — Journey:minSdk = 29。DaVinci/OIDC:minSdk = 28。 -
Ping Identity のブランディング — 必ず適用してください。
assets/Theme.kt.templateを読み込み、ui/theme/PingTheme.ktに書き出し、PLACEHOLDER_PACKAGE_NAMEを置き換えます。assets/ping_logo.pngをapp/src/main/res/drawable/にコピーします。 -
res/values/themes.xmlの親テーマ —Theme.Material3.DayNight.NoActionBarではなく、Theme.AppCompat.Light.NoActionBarを使用してください。Material3 の XML テーマにはcom.google.android.materialライブラリが必要ですが、依存関係のリストには含まれていません。Theme.AppCompat.Light.NoActionBarはactivity-composeを介して常に推移的に利用できます。誤った親テーマを使用すると、ビルド時に AAPT のリソースリンクエラーが発生します。 -
アプリアイコン —
@mipmap/ic_launcherは絶対に参照しないでください — 雛形として作成したプロジェクトには mipmap ディレクトリがありません。AndroidManifest.xmlでは、android:iconとandroid:roundIconの両方に(ステップ 7 でコピー済みの)@drawable/ping_logoを使用してください。存在しない mipmap を参照すると、上記のテーマエラーと同じ AAPT のリソースリンクエラーが発生します。
Ping Orchestration Android SDK — 統合ガイド
フローの種類
Journey(ネイティブ) — アプリ内で UI を構成し、コールバックを使用します。対象は PingOne AIC / PingAM です。
DaVinci — Journey と同様ですが、対象は PingOne DaVinci です。コレクターを使用し、serverUrl/realm/cookie はありません。
OIDC Web — Custom Tabs を使用したブラウザーです。リダイレクト URI のインテントフィルターを設定してください。
モジュール一覧
アーティファクト(com.pingidentity.sdks:<name>) | 追加する場合 |
|---|---|
journey | Journey フロー + すべてのコールバックタイプ |
davinci | DaVinci フロー + すべてのコレクタータイプ |
oidc | スタンドアロンの OidcWebClient |
external-idp | ソーシャルログイン(IdpCollector、SelectIdpCallback) |
fido | FIDO2 / パスキー |
protect | PingOne Protect のリスクシグナル |
binding | デバイスのバインドと署名 |
device-profile | デバイスのメタデータ |
oath | TOTP/HOTP — アーティファクトは oath であり、mfa-oath ではありません |
push | プッシュ MFA — アーティファクトは push であり、mfa-push ではありません |
recaptcha-enterprise | reCAPTCHA 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>
以下の 4 つのシンボルはすべてトップレベル拡張です。インポートしないと解決できません:
| シンボル | インポート |
|---|---|
journey.start(name) | import com.pingidentity.journey.start |
journey.user() | import com.pingidentity.journey.user |
continueNode.callbacks | import com.pingidentity.journey.plugin.callbacks |
Logger.STANDARD | import 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) } }
}
主なコレクタープロパティ:
| コレクター | 主なプロパティ | 入力 |
|---|---|---|
TextCollector | label、value、required | collector.value = text |
PasswordCollector | label、value | 先に collector.validate(pwd) を呼び出し、その後 collector.value = pwd |
SubmitCollector / FlowCollector | label | onNext を呼び出す |
LabelCollector | content | 表示のみ |
SingleSelectCollector | label、options: List<Option>、value: String | collector.value = option.value |
MultiSelectCollector | label、options: List<Option>、value: List<String> | collector.value = listOf(...) |
PhoneNumberCollector | label、defaultCountryCode、countryCode、phoneNumber | 両方のフィールドを設定 |
DeviceRegistrationCollector / DeviceAuthenticationCollector | devices: List<Device> | collector.value = device を設定してから onNext を呼び出す |
ProtectCollector | — | suspend collector.collect() を呼び出してから onNext を呼び出す |
FidoRegistrationCollector | — | suspend collector.register() を呼び出してから onNext を呼び出す |
FidoAuthenticationCollector | — | suspend collector.authenticate() を呼び出してから onNext を呼び出す |
IdpCollector | label、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() は suspend です。カスタムタブを起動し、リダイレクトを待ってから、コードをトークンに交換します。
11 — コレクター/コールバックのティア
| ティア | Journey の追加項目 | DaVinci の追加項目 | 追加モジュール |
|---|---|---|---|
| Basic | NameCallback、PasswordCallback、TextOutputCallback、ChoiceCallback | TextCollector、PasswordCollector、SubmitCollector、LabelCollector、FlowCollector | なし |
| Standard | + 検証済みコールバック、属性コールバック、ConfirmationCallback、TermsAndConditionsCallback、KbaCreateCallback、PollingWaitCallback、すべてのデバイス/FIDO/Protect/SelectIdp コールバック | + SingleSelectCollector、MultiSelectCollector、PhoneNumberCollector、デバイス/FIDO/Protect コレクター | fido、protect、binding、device-profile、external-idp |
| Full | + 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 を参照してください。インポートエラー、コールバックのプロパティ名の誤り、suspend メソッド名、再コンポジションのループ、Gradle/AGP のバージョンに関する問題、AAPT エラー、ブランディングコンポーネントの落とし穴について説明しています。
13 — MFA モジュール
プッシュ(FCM) — アーティファクト com.pingidentity.sdks:push:
val pushClient = PushClient { }
pushClient.addCredentialFromUri(uri) // QR scan
pushClient.processNotification(remoteMessage.data) // FCM
pushClient.approveNotification(notificationId)
OATH(TOTP/HOTP) — アーティファクト 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-auth | com.pingidentity.sdks:journey |
FRAuth.start() | journey.start("Login") |
FRUser.getCurrentUser() | journey.user() / daVinci.user() |
Node.getCallbacks() | continueNode.callbacks |
移行手順の詳細については、forgerock-to-ping-journey-migration スキルをお読みください。
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(名前が PingTextField のカラー定数と競合しないようにしています)、PingSecureField、PingErrorMessage、PingErrorCard、PingLoadingOverlay(Box(Modifier.fillMaxSize()) の最上位の兄弟要素として配置する必要があります)。
ログイン画面:Box(fillMaxSize) で Column(verticalScroll) をラップし、その上部に PingHeaderView、下部に CallbackNodeView/CollectorNodeView を配置します。読み込み中は PingLoadingOverlay を重ねて表示します。Checkbox/Switch には PingRed の色合いを適用します。