プッシュ通知 (FCM/APNs)
📍 このドキュメントの位置づけ
対象読者: プッシュ通知(FCM/APNs)の実装詳細を理解したい開発者
このドキュメントで学べること:
- FCM (Firebase Cloud Messaging) の実装詳細
- APNs (Apple Push Notification service) の実装詳細
- AuthenticationDeviceNotifier プラグインの実装方法
- 通知テンプレート管理
- JWT トークンキャッシュ戦略(APNs)
- エラーハンドリングとリトライ
前提知識:
- impl-12: Plugin実装ガイドの理解
- how-to-12: CIBA Flow (FIDO-UAF)の理解
- FCM/APNsの基礎知識
🏗️ プッシュ通知アーキテクチャ
idp-serverは、AuthenticationDeviceNotifierプラグインインターフェースでプッシュ通知を実装します。
通知フロー
┌─────────────────────────────────────────────────────────────┐
│ 1. CIBA認証リクエスト受信 │
│ - auth_req_id 発行 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. AuthenticationDeviceNotifier 選択 │
│ - NotificationChannel に基づいて選択 │
│ - fcm / apns / email / sms │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. プッシュ通知送信 │
│ - FCM: Firebase Admin SDK │
│ - APNs: HTTP/2 APIs + JWT認証 │
└─────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────── ───────────────────────┐
│ 4. ユーザー承認 │
│ - モバイルアプリでプッシュ通知受信 │
│ - 認証処理(FIDO-UAF等) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. トークン発行 │
│ - Poll/Ping方式でトークン取得 │
└─────────────────────────────────────────────────────────────┘
🔌 AuthenticationDeviceNotifier インターフェース
すべてのプッシュ通知実装は、このインターフェースを実装します。
public interface AuthenticationDeviceNotifier {
/**
* 通知チャネル名を返す
*/
NotificationChannel chanel();
/**
* プッシュ通知を送信
*
* @param tenant テナント情報
* @param device 認証デバイス(通知トークンを含む)
* @param configuration 認証設定(FCM/APNs設定を含む)
* @return 通知結果
*/
NotificationResult notify(
Tenant tenant,
AuthenticationDevice device,
AuthenticationExecutionConfig configuration);
}
参考実装: AuthenticationDeviceNotifier.java:25
NotificationResult
通知結果を表すクラスです。
public class NotificationResult {
boolean success;
String channel;
Map<String, Object> data;
String errorMessage;
public static NotificationResult success(String channel, Map<String, Object> data) {
return new NotificationResult(true, channel, data, null);
}
public static NotificationResult failure(String channel, String errorMessage) {
return new NotificationResult(false, channel, Map.of(), errorMessage);
}
public boolean isSuccess() {
return success;
}
public boolean isFailure() {
return !success;
}
}
参考実装: NotificationResult.java:21
NotificationTemplate
通知テンプレートを表すクラスです。
public class NotificationTemplate implements JsonReadable {
String sender;
String title;
String body;
public String optSender(String defaultValue) {
if (sender == null) {
return defaultValue;
}
return sender;
}
public String optTitle(String defaultValue) {
if (title == null) {
return defaultValue;
}
return title;
}
public String optBody(String defaultValue) {
if (body == null) {
return defaultValue;
}
return body;
}
}
参考実装: NotificationTemplate.java:21
📱 FCM実装
FcmNotifier
Firebase Admin SDK を使用してプッシュ通知を送信します。
public class FcmNotifier implements AuthenticationDeviceNotifier {
JsonConverter jsonConverter = JsonConverter.snakeCaseInstance();
Map<String, FirebaseMessaging> cache = new ConcurrentHashMap<>();
@Override
public NotificationChannel chanel() {
return new NotificationChannel("fcm");
}
@Override
public NotificationResult notify(
Tenant tenant,
AuthenticationDevice device,
AuthenticationExecutionConfig configuration) {
try {
// 1. 通知トークンの存在確認
if (!device.hasNotificationToken()) {
return NotificationResult.failure("fcm", "Device has no notification token");
}
// 2. FCM設定の取得
Object fcmConfigData = configuration.details().get("fcm");
if (fcmConfigData == null) {
return NotificationResult.failure("fcm", "FCM configuration not found");
}
FcmConfiguration fcmConfiguration =
jsonConverter.read(fcmConfigData, FcmConfiguration.class);
// 3. FirebaseMessaging インスタンス取得(キャッシュ)
FirebaseMessaging firebaseMessaging =
getOrInitFirebaseMessaging(tenant, fcmConfiguration);
// 4. 通知テンプレート取得
NotificationTemplate notificationTemplate = fcmConfiguration.findTemplate("default");
String notificationToken = device.notificationToken().value();
// 5. メッセージ構築
Message message =
Message.builder()
.setToken(notificationToken)
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.putData("sender", notificationTemplate.optSender(tenant.identifierValue()))
.putData("title", notificationTemplate.optTitle("Transaction Authentication"))
.putData("body", notificationTemplate.optBody("Please approve the transaction."))
.build())
.setApnsConfig(
ApnsConfig.builder()
.putHeader("apns-priority", "10")
.putCustomData("sender", notificationTemplate.optSender(tenant.identifierValue()))
.putCustomData("title", notificationTemplate.optTitle("Transaction Authentication"))
.putCustomData("body", notificationTemplate.optBody("Please approve the transaction."))
.setAps(Aps.builder().setContentAvailable(true).build())
.build())
.build();
// 6. 送信
String result = firebaseMessaging.send(message);
return NotificationResult.success("fcm", Map.of("result", result));
} catch (Exception e) {
return NotificationResult.failure("fcm", e.getMessage());
}
}
FirebaseMessaging getOrInitFirebaseMessaging(Tenant tenant, FcmConfiguration fcmConfiguration) {
return cache.computeIfAbsent(
tenant.identifierValue(),
(key) -> {
try {
String credential = fcmConfiguration.credential();
FirebaseOptions options =
FirebaseOptions.builder()
.setCredentials(
GoogleCredentials.fromStream(
new ByteArrayInputStream(credential.getBytes())))
.build();
FirebaseApp firebaseApp = FirebaseApp.initializeApp(options, tenant.identifierValue());
return FirebaseMessaging.getInstance(firebaseApp);
} catch (IOException e) {
throw new FcmRuntimeException(e);
}
});
}
}
参考実装: FcmNotifier.java:37
FCM設定
{
"fcm": {
"credential": "{Firebase Admin SDK JSON}",
"templates": {
"default": {
"sender": "MyBank",
"title": "Transaction Authentication",
"body": "Please approve the transaction to continue."
}
}
}
}
credential: Firebase Admin SDK のサービスアカウントキー(JSON形式)
重要なポイント
1. テナントごとのFirebaseApp
// ✅ テナントごとに FirebaseApp を初期化
FirebaseApp firebaseApp = FirebaseApp.initializeApp(options, tenant.identifierValue());
理由: マルチテナント環境で、テナントごとに異なるFirebaseプロジェクトを使用するため
2. Android / iOS 両対応
Message message = Message.builder()
.setToken(notificationToken)
.setAndroidConfig(...) // Android向け設定
.setApnsConfig(...) // iOS向け設定
.build();
FCMはAndroid と iOS の両方に対応しています。