Android OAuth (AppAuth Android)

認証ライブラリAndroidOAuth 2.0OpenID ConnectモバイルPKCEセキュリティ

ライブラリ

Android OAuth (AppAuth Android)

概要

AppAuth for Androidは、OpenID Foundation公式のAndroidネイティブアプリ向けOAuth 2.0およびOpenID Connectクライアントライブラリです。2025年現在、RFC 8252準拠のネイティブアプリ用OAuth実装として業界標準となっており、Google、Okta、Auth0、Microsoft Azure ADなど主要な認証プロバイダーとの互換性を提供しています。PKCEによるセキュアな認証フロー、Custom TabsまたはChromeブラウザーを使用したセキュアなブラウザー連携、動的クライアント登録、セッション管理機能を包括的にサポートします。AndroidのSecure App Linkとカスタムスキーム両方でのリダイレクトURI処理に対応し、エンタープライズ環境での証明書ピニングや高度なHTTP設定もサポートしています。

詳細

AppAuth for Android 0.11系は、RFC 8252「OAuth 2.0 for Native Apps」完全準拠の実装として、モバイルアプリケーションに最適化されたセキュアなOAuth 2.0フローを提供します。PKCE (Proof Key for Code Exchange) を自動的に実装し、認可コードインターセプト攻撃を防止します。AuthStateクラスによる包括的な認証状態管理により、アクセストークンの自動リフレッシュ、永続化、復元が可能です。Custom TabsまたはChromeブラウザーを使用することで、アプリ内Webビューの脆弱性を回避し、ユーザーの認証情報を保護します。動的クライアント登録により、事前設定なしでのOAuth プロバイダーとの統合も可能です。

主な特徴

  • RFC 8252完全準拠: ネイティブアプリ向けOAuth 2.0ベストプラクティス実装
  • PKCE自動実装: 認可コードインターセプト攻撃の自動防止
  • Custom Tabs統合: セキュアなブラウザー連携でWebビュー脆弱性を回避
  • 包括的状態管理: AuthStateによる認証状態の永続化と自動トークンリフレッシュ
  • マルチプロバイダー対応: Google、Microsoft、Auth0、Okta等との互換性
  • エンタープライズ対応: 証明書ピニング、カスタムHTTP設定、ブラウザー制限

メリット・デメリット

メリット

  • OpenID Foundation公式ライブラリで業界標準のセキュリティ実装を提供
  • PKCE自動実装により、追加実装なしで高いセキュリティレベルを実現
  • Custom Tabs使用でユーザー体験向上とセキュリティ強化を両立
  • 主要認証プロバイダーとの事前検証済み互換性で統合コスト削減
  • AuthStateクラスによる簡潔で堅牢な認証状態管理
  • エンタープライズ環境での高度なセキュリティ要件にも対応

デメリット

  • Custom TabsまたはChromeブラウザーへの依存性があり、古いデバイスでは制限
  • ネイティブアプリ専用でWebビューやハイブリッドアプリでは使用不可
  • OAuth 2.0以外の認証方式(独自認証等)には対応していない
  • 複雑な認証フローのカスタマイズには制約がある
  • マルチテナント環境での動的設定変更には別途実装が必要
  • ライブラリサイズが比較的大きく、アプリサイズへの影響を考慮が必要

参考ページ

書き方の例

基本的なセットアップとGradleでの依存関係追加

// app/build.gradle - AppAuth for Android依存関係追加
dependencies {
    implementation 'net.openid:appauth:0.11.1'
    
    // Android Support Library (必要に応じて)
    implementation 'androidx.browser:browser:1.4.0'
    implementation 'androidx.appcompat:appcompat:1.4.2'
}

// カスタムスキーム用のマニフェストプレースホルダー設定
android {
    defaultConfig {
        manifestPlaceholders = [
            'appAuthRedirectScheme': 'com.example.myapp'
        ]
    }
}
<!-- AndroidManifest.xml - リダイレクトURI受信設定 -->
<application>
    <!-- カスタムスキーム用RedirectUriReceiverActivity -->
    <activity
        android:name="net.openid.appauth.RedirectUriReceiverActivity"
        android:exported="true"
        tools:node="replace">
        <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="${appAuthRedirectScheme}" />
        </intent-filter>
    </activity>
    
    <!-- HTTPS App Link用設定(推奨) -->
    <activity
        android:name="net.openid.appauth.RedirectUriReceiverActivity"
        android:exported="true"
        tools:node="replace">
        <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="https"
                  android:host="myapp.example.com"
                  android:path="/oauth2redirect" />
        </intent-filter>
    </activity>
</application>
// res/raw/auth_config.json - OAuth設定ファイル
{
  "client_id": "your-client-id.apps.googleusercontent.com",
  "redirect_uri": "com.example.myapp:/oauth2redirect",
  "end_session_redirect_uri": "com.example.myapp:/logout",
  "authorization_scope": "openid email profile",
  "discovery_uri": "https://accounts.google.com/.well-known/openid-configuration",
  "authorization_endpoint_uri": "",
  "token_endpoint_uri": "",
  "https_required": true
}

OpenID Connect Discovery を使用した認証サービス設定

// AuthenticationManager.java - 認証管理クラス
import net.openid.appauth.*;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;

public class AuthenticationManager {
    private static final String TAG = "AuthManager";
    private static final String AUTH_STATE_KEY = "auth_state";
    private static final int RC_AUTH = 100;
    
    private Context mContext;
    private AuthorizationService mAuthService;
    private AuthState mAuthState;
    private AuthorizationServiceConfiguration mServiceConfig;
    
    // OAuth設定
    private static final String CLIENT_ID = "your-client-id.apps.googleusercontent.com";
    private static final Uri REDIRECT_URI = Uri.parse("com.example.myapp:/oauth2redirect");
    private static final Uri DISCOVERY_URI = Uri.parse("https://accounts.google.com/.well-known/openid-configuration");
    private static final String SCOPE = "openid email profile";
    
    public AuthenticationManager(Context context) {
        mContext = context;
        mAuthService = new AuthorizationService(context);
        mAuthState = readAuthState();
        
        // OpenID Connect Discovery実行
        initializeOAuthConfiguration();
    }
    
    private void initializeOAuthConfiguration() {
        // サービス設定をDiscoveryで取得
        AuthorizationServiceConfiguration.fetchFromIssuer(
            DISCOVERY_URI,
            new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() {
                @Override
                public void onFetchConfigurationCompleted(
                        @Nullable AuthorizationServiceConfiguration serviceConfiguration,
                        @Nullable AuthorizationException ex) {
                    if (ex != null) {
                        Log.e(TAG, "OAuth設定の取得に失敗: " + ex.getMessage());
                        return;
                    }
                    
                    mServiceConfig = serviceConfiguration;
                    Log.i(TAG, "OAuth設定の取得完了");
                    
                    // AuthStateが空の場合は新規作成
                    if (mAuthState.getAuthorizationServiceConfiguration() == null) {
                        mAuthState = new AuthState(serviceConfiguration);
                        writeAuthState(mAuthState);
                    }
                }
            }
        );
    }
    
    // 手動でのサービス設定(Discovery使用不可の場合)
    private void configureOAuthManually() {
        mServiceConfig = new AuthorizationServiceConfiguration(
            Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"), // 認可エンドポイント
            Uri.parse("https://oauth2.googleapis.com/token")            // トークンエンドポイント
        );
        
        mAuthState = new AuthState(mServiceConfig);
        writeAuthState(mAuthState);
    }
    
    // 認証状態の永続化
    private void writeAuthState(@NonNull AuthState state) {
        SharedPreferences authPrefs = mContext.getSharedPreferences("auth_state", Context.MODE_PRIVATE);
        authPrefs.edit()
                .putString(AUTH_STATE_KEY, state.jsonSerializeString())
                .apply();
    }
    
    // 認証状態の復元
    @NonNull
    private AuthState readAuthState() {
        SharedPreferences authPrefs = mContext.getSharedPreferences("auth_state", Context.MODE_PRIVATE);
        String stateJson = authPrefs.getString(AUTH_STATE_KEY, null);
        if (stateJson != null) {
            try {
                return AuthState.jsonDeserialize(stateJson);
            } catch (JSONException ex) {
                Log.w(TAG, "認証状態の復元に失敗、新規作成: " + ex.getMessage());
            }
        }
        return new AuthState();
    }
    
    // 認証状態確認
    public boolean isAuthenticated() {
        return mAuthState.isAuthorized() && !mAuthState.getNeedsTokenRefresh();
    }
    
    // 現在のユーザー情報取得
    @Nullable
    public String getCurrentUserId() {
        return mAuthState.getIdToken();
    }
    
    @Nullable
    public String getAccessToken() {
        return mAuthState.getAccessToken();
    }
}

認証フローの実装とPKCE対応

// AuthenticationManager.java (続き) - 認証フロー実装
public class AuthenticationManager {
    
    // 認証開始(PKCE自動実装)
    public void startAuthentication(Activity activity) {
        if (mServiceConfig == null) {
            Log.e(TAG, "OAuth設定が未初期化");
            return;
        }
        
        // 認証リクエスト作成(PKCEは自動的に実装される)
        AuthorizationRequest.Builder authRequestBuilder = new AuthorizationRequest.Builder(
            mServiceConfig,
            CLIENT_ID,
            ResponseTypeValues.CODE,
            REDIRECT_URI
        );
        
        // オプションパラメーター設定
        AuthorizationRequest authRequest = authRequestBuilder
            .setScope(SCOPE)
            .setLoginHint("[email protected]") // オプション: ログインヒント
            .setPrompt("select_account")      // オプション: アカウント選択強制
            .build();
        
        // ブラウザーでの認証開始
        Intent authIntent = mAuthService.getAuthorizationRequestIntent(authRequest);
        activity.startActivityForResult(authIntent, RC_AUTH);
    }
    
    // 認証レスポンス処理
    public void handleAuthorizationResponse(Intent data, AuthCallback callback) {
        AuthorizationResponse authResponse = AuthorizationResponse.fromIntent(data);
        AuthorizationException authException = AuthorizationException.fromIntent(data);
        
        // 認証状態更新
        mAuthState.update(authResponse, authException);
        writeAuthState(mAuthState);
        
        if (authResponse != null) {
            // 認証成功:トークン交換実行
            exchangeTokens(authResponse, callback);
        } else {
            // 認証失敗
            Log.e(TAG, "認証失敗: " + (authException != null ? authException.getMessage() : "Unknown error"));
            callback.onAuthError(authException);
        }
    }
    
    // トークン交換処理
    private void exchangeTokens(AuthorizationResponse authResponse, AuthCallback callback) {
        TokenRequest tokenRequest = authResponse.createTokenExchangeRequest();
        
        mAuthService.performTokenRequest(
            tokenRequest,
            new AuthorizationService.TokenResponseCallback() {
                @Override
                public void onTokenRequestCompleted(
                        @Nullable TokenResponse tokenResponse,
                        @Nullable AuthorizationException ex) {
                    
                    // 認証状態更新
                    mAuthState.update(tokenResponse, ex);
                    writeAuthState(mAuthState);
                    
                    if (tokenResponse != null) {
                        Log.i(TAG, "トークン交換成功");
                        callback.onAuthSuccess(tokenResponse);
                    } else {
                        Log.e(TAG, "トークン交換失敗: " + (ex != null ? ex.getMessage() : "Unknown error"));
                        callback.onAuthError(ex);
                    }
                }
            }
        );
    }
    
    // フレッシュトークンでのAPI実行
    public void performActionWithFreshTokens(AuthStateAction action) {
        mAuthState.performActionWithFreshTokens(
            mAuthService,
            new AuthState.AuthStateAction() {
                @Override
                public void execute(
                        @Nullable String accessToken,
                        @Nullable String idToken,
                        @Nullable AuthorizationException ex) {
                    
                    if (ex != null) {
                        Log.e(TAG, "トークン更新失敗: " + ex.getMessage());
                        action.onError(ex);
                        return;
                    }
                    
                    // フレッシュトークンでアクション実行
                    action.onSuccess(accessToken, idToken);
                }
            }
        );
    }
    
    // ログアウト処理
    public void logout() {
        // 認証状態をクリア
        mAuthState = new AuthState();
        writeAuthState(mAuthState);
        
        // AuthorizationServiceを破棄
        mAuthService.dispose();
        mAuthService = new AuthorizationService(mContext);
        
        Log.i(TAG, "ログアウト完了");
    }
    
    // リソース解放
    public void dispose() {
        if (mAuthService != null) {
            mAuthService.dispose();
        }
    }
    
    // コールバックインターフェース
    public interface AuthCallback {
        void onAuthSuccess(TokenResponse tokenResponse);
        void onAuthError(AuthorizationException exception);
    }
    
    // アクションインターフェース
    public interface AuthStateAction {
        void onSuccess(String accessToken, String idToken);
        void onError(AuthorizationException exception);
    }
}

ActivityでのOAuth統合とライフサイクル管理

// MainActivity.java - メインActivity統合例
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.TokenResponse;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static final int RC_AUTH = 100;
    
    private AuthenticationManager mAuthManager;
    private Button mSignInButton;
    private Button mSignOutButton;
    private Button mApiCallButton;
    private TextView mUserInfoTextView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // AuthenticationManager初期化
        mAuthManager = new AuthenticationManager(this);
        
        // UI初期化
        initializeUI();
        
        // 認証状態確認とUI更新
        updateUI();
    }
    
    private void initializeUI() {
        mSignInButton = findViewById(R.id.sign_in_button);
        mSignOutButton = findViewById(R.id.sign_out_button);
        mApiCallButton = findViewById(R.id.api_call_button);
        mUserInfoTextView = findViewById(R.id.user_info_text);
        
        // サインインボタン
        mSignInButton.setOnClickListener(v -> {
            Log.i(TAG, "サインイン開始");
            mAuthManager.startAuthentication(this);
        });
        
        // サインアウトボタン
        mSignOutButton.setOnClickListener(v -> {
            Log.i(TAG, "サインアウト実行");
            mAuthManager.logout();
            updateUI();
            Toast.makeText(this, "サインアウト完了", Toast.LENGTH_SHORT).show();
        });
        
        // API呼び出しボタン
        mApiCallButton.setOnClickListener(v -> {
            performApiCall();
        });
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        
        if (requestCode == RC_AUTH) {
            // 認証レスポンス処理
            mAuthManager.handleAuthorizationResponse(data, new AuthenticationManager.AuthCallback() {
                @Override
                public void onAuthSuccess(TokenResponse tokenResponse) {
                    runOnUiThread(() -> {
                        Log.i(TAG, "認証成功");
                        updateUI();
                        Toast.makeText(MainActivity.this, "サインイン成功", Toast.LENGTH_SHORT).show();
                        
                        // ユーザー情報取得
                        fetchUserInfo();
                    });
                }
                
                @Override
                public void onAuthError(AuthorizationException exception) {
                    runOnUiThread(() -> {
                        Log.e(TAG, "認証エラー: " + exception.getMessage());
                        updateUI();
                        Toast.makeText(MainActivity.this, "サインインエラー: " + exception.getMessage(), 
                                Toast.LENGTH_LONG).show();
                    });
                }
            });
        }
    }
    
    // UI状態更新
    private void updateUI() {
        boolean isAuthenticated = mAuthManager.isAuthenticated();
        
        mSignInButton.setVisibility(isAuthenticated ? android.view.View.GONE : android.view.View.VISIBLE);
        mSignOutButton.setVisibility(isAuthenticated ? android.view.View.VISIBLE : android.view.View.GONE);
        mApiCallButton.setVisibility(isAuthenticated ? android.view.View.VISIBLE : android.view.View.GONE);
        
        if (!isAuthenticated) {
            mUserInfoTextView.setText("サインインしてください");
        }
    }
    
    // ユーザー情報取得
    private void fetchUserInfo() {
        mAuthManager.performActionWithFreshTokens(new AuthenticationManager.AuthStateAction() {
            @Override
            public void onSuccess(String accessToken, String idToken) {
                // Google User Info API呼び出し例
                fetchUserInfoFromAPI(accessToken);
            }
            
            @Override
            public void onError(AuthorizationException exception) {
                runOnUiThread(() -> {
                    Log.e(TAG, "トークン取得エラー: " + exception.getMessage());
                    mUserInfoTextView.setText("ユーザー情報取得エラー");
                });
            }
        });
    }
    
    // API呼び出し実装例
    private void performApiCall() {
        mAuthManager.performActionWithFreshTokens(new AuthenticationManager.AuthStateAction() {
            @Override
            public void onSuccess(String accessToken, String idToken) {
                // 保護されたAPIを呼び出し
                callProtectedAPI(accessToken);
            }
            
            @Override
            public void onError(AuthorizationException exception) {
                runOnUiThread(() -> {
                    Log.e(TAG, "API呼び出しエラー: " + exception.getMessage());
                    Toast.makeText(MainActivity.this, "API呼び出しエラー", Toast.LENGTH_SHORT).show();
                });
            }
        });
    }
    
    // Google User Info API呼び出し
    private void fetchUserInfoFromAPI(String accessToken) {
        // HTTP客户端实现(例:OkHttp、Volley等)
        // 実装例:https://www.googleapis.com/oauth2/v2/userinfo
        new Thread(() -> {
            try {
                // HTTPリクエスト実装
                String userInfo = makeHttpRequest("https://www.googleapis.com/oauth2/v2/userinfo", accessToken);
                
                runOnUiThread(() -> {
                    mUserInfoTextView.setText("ユーザー情報: " + userInfo);
                });
            } catch (Exception e) {
                Log.e(TAG, "ユーザー情報取得エラー", e);
                runOnUiThread(() -> {
                    mUserInfoTextView.setText("ユーザー情報取得失敗");
                });
            }
        }).start();
    }
    
    // 保護されたAPI呼び出し
    private void callProtectedAPI(String accessToken) {
        // 任意の保護されたAPIエンドポイント呼び出し実装
        new Thread(() -> {
            try {
                String response = makeHttpRequest("https://api.example.com/protected", accessToken);
                
                runOnUiThread(() -> {
                    Log.i(TAG, "API応答: " + response);
                    Toast.makeText(MainActivity.this, "API呼び出し成功", Toast.LENGTH_SHORT).show();
                });
            } catch (Exception e) {
                Log.e(TAG, "API呼び出しエラー", e);
                runOnUiThread(() -> {
                    Toast.makeText(MainActivity.this, "API呼び出し失敗", Toast.LENGTH_SHORT).show();
                });
            }
        }).start();
    }
    
    // HTTP request helper method
    private String makeHttpRequest(String url, String accessToken) throws Exception {
        // 実際のHTTPクライアント実装(OkHttp、HttpURLConnection等)
        java.net.URL requestUrl = new java.net.URL(url);
        java.net.HttpURLConnection connection = (java.net.HttpURLConnection) requestUrl.openConnection();
        connection.setRequestProperty("Authorization", "Bearer " + accessToken);
        connection.setRequestMethod("GET");
        
        // レスポンス読み取り実装
        if (connection.getResponseCode() == 200) {
            java.io.BufferedReader reader = new java.io.BufferedReader(
                new java.io.InputStreamReader(connection.getInputStream()));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            reader.close();
            return response.toString();
        } else {
            throw new Exception("HTTP " + connection.getResponseCode() + ": " + connection.getResponseMessage());
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mAuthManager != null) {
            mAuthManager.dispose();
        }
    }
}

エンタープライズセキュリティ設定と高度な構成

// EnterpriseAuthConfiguration.java - エンタープライズ設定
import net.openid.appauth.*;
import net.openid.appauth.browser.*;
import android.content.Context;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class EnterpriseAuthConfiguration {
    
    // 証明書ピニング付きHTTPコネクション設定
    public static AuthorizationService createSecureAuthService(Context context) {
        AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder()
            .setConnectionBuilder(new SecureConnectionBuilder())
            .setBrowserMatcher(createSecureBrowserMatcher())
            .setSkipIssuerHttpsCheck(false) // 本番環境では必ずfalse
            .build();
        
        return new AuthorizationService(context, appAuthConfig);
    }
    
    // セキュアなHTTP接続ビルダー
    private static class SecureConnectionBuilder implements ConnectionBuilder {
        @Override
        public HttpURLConnection openConnection(Uri uri) throws IOException {
            URL url = new URL(uri.toString());
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            
            if (connection instanceof HttpsURLConnection) {
                HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
                
                // カスタムSSLSocketFactory設定(証明書ピニング)
                SSLSocketFactory customSSLFactory = createPinnedSSLSocketFactory();
                if (customSSLFactory != null) {
                    httpsConnection.setSSLSocketFactory(customSSLFactory);
                }
                
                // カスタムHostnameVerifier設定(オプション)
                // httpsConnection.setHostnameVerifier(createCustomHostnameVerifier());
            }
            
            // 追加のHTTPヘッダー設定
            connection.setRequestProperty("User-Agent", "MyEnterpriseApp/1.0");
            connection.setConnectTimeout(30000); // 30秒タイムアウト
            connection.setReadTimeout(30000);
            
            return connection;
        }
        
        private SSLSocketFactory createPinnedSSLSocketFactory() {
            // 証明書ピニング実装
            // 実際の実装ではOkHttpのCertificatePinner等を使用
            return null; // プレースホルダー
        }
    }
    
    // セキュアなブラウザー選択設定
    private static BrowserMatcher createSecureBrowserMatcher() {
        // Chrome Custom TabsまたはSamsung Browser Custom Tabsのみ許可
        return new BrowserAllowList(
            VersionedBrowserMatcher.CHROME_CUSTOM_TAB,
            VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB
        );
    }
    
    // 特定のブラウザーバージョンをブロック
    private static BrowserMatcher createRestrictiveBrowserMatcher() {
        // 古いSamsung Browserバージョンをブロック
        return new BrowserDenyList(
            new VersionedBrowserMatcher(
                Browsers.SBrowser.PACKAGE_NAME,
                Browsers.SBrowser.SIGNATURE_SET,
                true, // Custom Tab使用時
                VersionRange.atMost("5.3") // 5.3以下のバージョンをブロック
            )
        );
    }
    
    // 動的クライアント登録の実装
    public static void performDynamicClientRegistration(
            AuthorizationServiceConfiguration serviceConfig,
            Uri redirectUri,
            RegistrationCallback callback) {
        
        RegistrationRequest registrationRequest = new RegistrationRequest.Builder(
            serviceConfig,
            java.util.Arrays.asList(redirectUri)
        )
        .setTokenEndpointAuthenticationMethod(ClientSecretBasic.NAME)
        .setApplicationType(RegistrationRequest.APPLICATION_TYPE_NATIVE)
        .build();
        
        AuthorizationService service = new AuthorizationService(null); // contextは実際のものを使用
        service.performRegistrationRequest(
            registrationRequest,
            new AuthorizationService.RegistrationResponseCallback() {
                @Override
                public void onRegistrationRequestCompleted(
                        @androidx.annotation.Nullable RegistrationResponse resp,
                        @androidx.annotation.Nullable AuthorizationException ex) {
                    
                    if (resp != null) {
                        // 登録成功:クライアントIDとシークレットを保存
                        AuthState authState = new AuthState(resp);
                        callback.onRegistrationSuccess(authState, resp.clientId, resp.clientSecret);
                    } else {
                        // 登録失敗
                        callback.onRegistrationError(ex);
                    }
                }
            }
        );
    }
    
    public interface RegistrationCallback {
        void onRegistrationSuccess(AuthState authState, String clientId, String clientSecret);
        void onRegistrationError(AuthorizationException exception);
    }
    
    // クライアントシークレット認証(非推奨:デモ目的のみ)
    public static void performTokenRequestWithClientSecret(
            AuthorizationService service,
            TokenRequest tokenRequest,
            String clientSecret,
            AuthorizationService.TokenResponseCallback callback) {
        
        // ClientSecretBasic認証(HTTPBasic認証)
        ClientAuthentication clientAuth = new ClientSecretBasic(clientSecret);
        
        service.performTokenRequest(tokenRequest, clientAuth, callback);
    }
    
    // エンドセッション(ログアウト)実装
    public static void performEndSession(
            Activity activity,
            AuthorizationServiceConfiguration serviceConfig,
            String idToken,
            Uri postLogoutRedirectUri) {
        
        EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(serviceConfig)
            .setIdTokenHint(idToken)
            .setPostLogoutRedirectUri(postLogoutRedirectUri)
            .build();
        
        AuthorizationService authService = new AuthorizationService(activity);
        Intent endSessionIntent = authService.getEndSessionRequestIntent(endSessionRequest);
        activity.startActivityForResult(endSessionIntent, 101); // RC_END_SESSION
    }
}

エラーハンドリングとログ管理

// AuthErrorHandler.java - エラー処理とログ管理
import net.openid.appauth.AuthorizationException;
import android.util.Log;

public class AuthErrorHandler {
    private static final String TAG = "AuthErrorHandler";
    
    // OAuth例外の詳細分析
    public static void handleAuthorizationException(AuthorizationException ex) {
        if (ex == null) return;
        
        Log.e(TAG, "認証エラー詳細:");
        Log.e(TAG, "  エラーコード: " + ex.code);
        Log.e(TAG, "  エラーメッセージ: " + ex.getMessage());
        Log.e(TAG, "  エラー詳細: " + ex.error);
        Log.e(TAG, "  エラー説明: " + ex.errorDescription);
        Log.e(TAG, "  エラーURI: " + ex.errorUri);
        
        // エラータイプ別処理
        switch (ex.type) {
            case AuthorizationException.TYPE_OAUTH_AUTHORIZATION_ERROR:
                handleOAuthAuthorizationError(ex);
                break;
            case AuthorizationException.TYPE_OAUTH_TOKEN_ERROR:
                handleOAuthTokenError(ex);
                break;
            case AuthorizationException.TYPE_OAUTH_REGISTRATION_ERROR:
                handleRegistrationError(ex);
                break;
            case AuthorizationException.TYPE_GENERAL_ERROR:
                handleGeneralError(ex);
                break;
            default:
                Log.e(TAG, "未知のエラータイプ: " + ex.type);
        }
    }
    
    private static void handleOAuthAuthorizationError(AuthorizationException ex) {
        Log.w(TAG, "OAuth認証エラー処理");
        
        // 一般的な認証エラー処理
        if ("access_denied".equals(ex.error)) {
            Log.i(TAG, "ユーザーが認証を拒否");
        } else if ("invalid_request".equals(ex.error)) {
            Log.e(TAG, "無効なリクエスト");
        } else if ("unauthorized_client".equals(ex.error)) {
            Log.e(TAG, "クライアント認証失敗");
        } else if ("unsupported_response_type".equals(ex.error)) {
            Log.e(TAG, "サポートされていないレスポンスタイプ");
        } else if ("invalid_scope".equals(ex.error)) {
            Log.e(TAG, "無効なスコープ");
        } else if ("server_error".equals(ex.error)) {
            Log.e(TAG, "認証サーバーエラー");
        } else if ("temporarily_unavailable".equals(ex.error)) {
            Log.w(TAG, "認証サーバー一時利用不可");
        }
    }
    
    private static void handleOAuthTokenError(AuthorizationException ex) {
        Log.w(TAG, "OAuthトークンエラー処理");
        
        if ("invalid_request".equals(ex.error)) {
            Log.e(TAG, "無効なトークンリクエスト");
        } else if ("invalid_client".equals(ex.error)) {
            Log.e(TAG, "クライアント認証失敗");
        } else if ("invalid_grant".equals(ex.error)) {
            Log.e(TAG, "無効な認証グラント");
        } else if ("unauthorized_client".equals(ex.error)) {
            Log.e(TAG, "認可されていないクライアント");
        } else if ("unsupported_grant_type".equals(ex.error)) {
            Log.e(TAG, "サポートされていないグラントタイプ");
        } else if ("invalid_scope".equals(ex.error)) {
            Log.e(TAG, "無効なスコープ");
        }
    }
    
    private static void handleRegistrationError(AuthorizationException ex) {
        Log.w(TAG, "動的クライアント登録エラー処理");
        
        if ("invalid_redirect_uri".equals(ex.error)) {
            Log.e(TAG, "無効なリダイレクトURI");
        } else if ("invalid_client_metadata".equals(ex.error)) {
            Log.e(TAG, "無効なクライアントメタデータ");
        }
    }
    
    private static void handleGeneralError(AuthorizationException ex) {
        Log.w(TAG, "一般エラー処理");
        
        // ネットワーク関連エラー
        if (ex.getCause() instanceof java.net.UnknownHostException) {
            Log.e(TAG, "ネットワーク接続エラー:ホストが見つかりません");
        } else if (ex.getCause() instanceof java.net.SocketTimeoutException) {
            Log.e(TAG, "ネットワークタイムアウト");
        } else if (ex.getCause() instanceof javax.net.ssl.SSLException) {
            Log.e(TAG, "SSL/TLS接続エラー");
        }
    }
    
    // ユーザーフレンドリーなエラーメッセージ生成
    public static String getUserFriendlyErrorMessage(AuthorizationException ex) {
        if (ex == null) return "不明なエラーが発生しました";
        
        switch (ex.type) {
            case AuthorizationException.TYPE_OAUTH_AUTHORIZATION_ERROR:
                if ("access_denied".equals(ex.error)) {
                    return "認証がキャンセルされました";
                } else if ("server_error".equals(ex.error)) {
                    return "認証サーバーでエラーが発生しました。しばらく時間をおいて再試行してください";
                }
                break;
                
            case AuthorizationException.TYPE_OAUTH_TOKEN_ERROR:
                if ("invalid_grant".equals(ex.error)) {
                    return "認証期限が切れました。再度サインインしてください";
                }
                break;
                
            case AuthorizationException.TYPE_GENERAL_ERROR:
                if (ex.getCause() instanceof java.net.UnknownHostException) {
                    return "ネットワーク接続を確認してください";
                } else if (ex.getCause() instanceof java.net.SocketTimeoutException) {
                    return "接続がタイムアウトしました。再試行してください";
                }
                break;
        }
        
        return "認証エラーが発生しました: " + ex.getMessage();
    }
    
    // セキュリティ監査ログ
    public static void logSecurityEvent(String event, String details) {
        Log.i(TAG, String.format("[SECURITY] %s - %s", event, details));
        
        // 実際の実装では、セキュリティログを専用システムに送信
        // analyticsService.trackSecurityEvent(event, details);
    }
}