不正なOAuth再登録を防ぐ - HMAC-SHA256による決定論的ユーザー識別の設計
📝 この記事について
OAuth認証(今回はGoogle)を使ったSaaSで、
一度退会したユーザーの再登録を制限したい場合の
技術的な実装パターンを検証してみました。
🤔 課題:退会後の即座な再登録
OAuth認証で退会処理を実装する際、
退会したユーザーの再登録を制限したいというニーズがあります。
想定されるユースケース
- 無料体験の不正利用防止
- BANユーザーの再登録防止
- 重複アカウント防止
問題の発生
単純に実装すると以下の問題が発生します:
- サーバー側でユーザーレコードを物理削除
- 同じOAuthアカウントで再度ログイン
- 新規ユーザーとして登録される
- 制限が無効化されてしまう ⚠️
🔍 解決策の検討
案1: 論理削除で管理
deleted_atカラムを追加して、削除フラグで管理する方法。
メリット:
- 実装がシンプル
- ユーザー情報が残るので分析可能
デメリット(却下理由):
- カスケード削除が使えなくなる
- 退会ユーザーの個人情報(名前、メールアドレス等)を保持
- プライバシー的な懸念
案2: 識別子のハッシュを保存(採用)
アプローチ:
- ユーザー情報は物理削除
- GoogleIDのハッシュ値だけを退会記録として保存
- 「同一アカウントからのアクセス」だけを判定可能にする
利点:
- ユーザーの個人情報は完全削除
- 最小限のデータ保持
- プライバシーに配慮しつつ不正防止も実現
GoogleIDとは:
OAuth 2.0のIDトークンに含まれるsubクレーム(Subject Identifier)のこと。
108123456789012345678のようなランダムな数値列で、
このID単体では氏名やメールアドレスは分からないが、
Googleアカウントとは一意に紐づいている。
🔑 ハッシュアルゴリズムの選択
なぜHMAC-SHA256なのか
今回の要件:
- ✅ 同じGoogle ID → 必ず同じ値になる(決定論的)
- ✅ データベースで検索できる(O(1))
- ✅ 復号不可能
- ✅ 秘密鍵で保護
SHA-256単体では不十分
GoogleIDは21桁程度の数値文字列(0-9のみ)でエントロピーが低く:
- 約10^21通りのパターン
- レインボーテーブルで事前計算可能
- 総当たり攻撃のコストが低い
SHA-256単体でハッシュ化しても、この脆弱性は解消されません。
HMAC-SHA256の採用理由
- 秘密鍵付き: 秘密鍵なしでは元のIDを復元不可能
- レインボーテーブル耐性: 事前計算攻撃に強い
- 決定論的: 同じ入力と秘密鍵から常に同じ出力
HMAC-SHA256の業界実績
HMAC-SHA256は、
決定論的ハッシュによるユーザー識別が必要な場面で採用される暗号化方式です。
Google Cloud
Google Cloudの公式ドキュメントで仮名化の手法としてHMAC-SHA256を紹介。
📚 Pseudonymization - Google Cloud
Elastic(GDPR対応の仮名化)
GDPR準拠のための個人データ保護手法として、HMACによる仮名化を詳解。
Logstashでの実装例とともに、キーローテーションやセキュリティ要件についても言及。
📚 GDPR Personal Data Pseudonymization - Elastic Blog
GDPR/法務的な位置づけ
GDPR第25条(Data protection by design and by default)において、
仮名化(pseudonymization)は推奨される技術的措置として明示されています。
HMAC-SHA256は、この仮名化技術の実装方法として以下の特性を持ちます:
技術的特性:
- 秘密鍵なしでは元データへの復元が不可能
- 同一入力に対して常に同一出力(決定論的ハッシュ)
- レインボーテーブル攻撃への耐性
GDPR適用上のメリット:
- データ最小化の原則に適合
- 不正防止(fraud prevention)目的で使用可能
- 正当な利益(legitimate interest)の根拠として扱える
参考資料
- 📚 EDPB Guidelines 4/2019 on Article 25 - 仮名化を技術的措置として推奨
- 📚 Pseudonymisation - Łukasz Olejnik - HMAC-SHA256の技術的解説
- 📚 Pseudonymization - Google Cloud - 実装例とGDPR準拠のガイダンス
RFC標準化されている
HMACはRFC 2104で標準化されている暗号方式です。
💻 実装例(Spring Boot)
前提条件
- Spring Boot 3.x + Java 17以降
- Spring Security + OAuth 2.0 Client
- PostgreSQL
データベース
CREATE TABLE withdrawn_google_accounts (
id UUID PRIMARY KEY,
hashed_google_id VARCHAR(64) NOT NULL UNIQUE,
withdrawn_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_hashed_google_id
ON withdrawn_google_accounts(hashed_google_id);
ハッシュ化コンポーネント
@Component
public class GoogleIdHasher {
@Value("${app.security.google-id-secret}")
private String googleIdSecret;
public String hashGoogleId(String googleId) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
googleIdSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
hmac.init(secretKey);
byte[] hash = hmac.doFinal(
googleId.getBytes(StandardCharsets.UTF_8)
);
return HexFormat.of().formatHex(hash);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("ハッシュ化に失敗", e);
}
}
}
退会時の処理
@Transactional
public void withdraw(String googleId) {
// ユーザー削除
userRepository.deleteByGoogleId(googleId);
// ハッシュ化して退会記録を保存
String hashedId = googleIdHasher.hashGoogleId(googleId);
withdrawnRepository.save(hashedId, LocalDateTime.now());
}
ログイン時のチェック
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oauth2User = super.loadUser(userRequest);
String googleId = oauth2User.getAttribute("sub");
// 退会記録チェック
String hashedId = googleIdHasher.hashGoogleId(googleId);
if (withdrawnRepository.existsByHashedGoogleId(hashedId)) {
throw new OAuth2AuthenticationException("このアカウントは退会済みです");
}
return oauth2User;
}
設定
application.yml:
app:
security:
google-id-secret: ${GOOGLE_ID_SECRET:dev-secret-change-in-production}
🔒 プライバシーへの配慮
データ最小化の原則
元のGoogleIDをそのまま保存しない理由:
- データベースから元のIDを復元できない
- Googleアカウントとの直接的な紐付けを遮断
- 「退会記録の確認」という目的だけに使える形で保存
HMAC-SHA256により秘密鍵付きで不可逆変換することで、
必要最小限の情報のみを保持しています。
データ保持期間の制限
一定経過したレコードはバッチ処理で定期的に削除。
無期限保持ではなく期限を設けることで、
不正利用の抑止とプライバシー保護のバランスを取っています。
プライバシーポリシーへの記載
法律上は個人データに該当する可能性があるため、
プライバシーポリシーへの記載が推奨されます。
📚 まとめ
OAuth認証での再登録制限について、実装パターンを検証しました。
今回のアプローチ:
- ユーザー情報は物理削除
- HMAC-SHA256で識別子をハッシュ化
- 最小限の情報で再登録を防ぐ
- データ保持期間を制限
今回は「同一アカウントかどうかを検索で判定する」という要件のため、
決定論的ハッシュであるHMAC-SHA256を選択しました。
パスワード保存など検索が不要なケースや、
別のセキュリティ要件がある場合は、また別の検討が必要になりそうです。
Discussion