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. 01Androidプロジェクト一式の生成
  2. 02Kotlin統合ファイルの生成
  3. 03Journey、DaVinci、OIDC Webの設定
  4. 04コールバックとコレクターの表示
  5. 05OAuthとGradle設定の検証

活用シーン

  1. 01Android認証サンプルの作成
  2. 02既存アプリへのPing SDK認証の追加
  3. 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.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 のみ)。

必要なパラメーターをすべて 1 つの 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 スコープ
serverUrlJourney のみ—PingAM/AIC のベース URL(末尾に / を付けないでください)
realmJourney のみalpha認証レルム
cookieNameJourney のみiPlanetDirectoryProSSO Cookie 名
journeyNameJourney のみLoginJourney ツリー名
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 では FirIncompatibleClassExpressionChecker による K2 クラッシュが発生します)。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.Material3.DayNight.NoActionBar ではなく、Theme.AppCompat.Light.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 の両方に(ステップ 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>)追加する場合
journeyJourney フロー + すべてのコールバックタイプ
davinciDaVinci フロー + すべてのコレクタータイプ
oidcスタンドアロンの OidcWebClient
external-idpソーシャルログイン(IdpCollector、SelectIdpCallback)
fidoFIDO2 / パスキー
protectPingOne Protect のリスクシグナル
bindingデバイスのバインドと署名
device-profileデバイスのメタデータ
oathTOTP/HOTP — アーティファクトは oath であり、mfa-oath ではありません
pushプッシュ MFA — アーティファクトは 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>

以下の 4 つのシンボルはすべてトップレベル拡張です。インポートしないと解決できません:

シンボルインポート
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 / FlowCollectorlabelonNext を呼び出す
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、iconUrlsuspend 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 の追加項目追加モジュール
BasicNameCallback、PasswordCallback、TextOutputCallback、ChoiceCallbackTextCollector、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-authcom.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 の色合いを適用します。

コメント