Android OAuth (AppAuth Android)
ライブラリ
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);
}
}