Skip to content

Commit 84e6451

Browse files
포스트 날짜 수정: '2025-06-21 10:00:00 +0900'에서 '2025-06-21 09:30:00 +0900'으로 변경하여 정확한 시간 정보를 반영하였습니다.
1 parent c049a9e commit 84e6451

2 files changed

Lines changed: 374 additions & 0 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
layout: post
3+
title: "Unity + Firebase Authentication Journey: From Anonymous to Account Linking"
4+
date: 2025-06-21 10:00:00 +0900
5+
categories: [Development, Unity]
6+
tags: [Unity, Firebase, Authentication, AccountLinking, AWS, Lambda, AuthSystem, GameDevelopment]
7+
author: "Kevin Park"
8+
excerpt: "Seamless experience for both guests and members! From Firebase Anonymous Auth to Account Linking - real implementation trials and solutions"
9+
image: "/assets/images/posts/firebase-auth-journey/hero.png"
10+
mermaid: true
11+
lang: en
12+
---
13+
14+
# Unity + Firebase Authentication Journey: From Anonymous to Account Linking
15+
16+
![Unity Firebase Authentication System](/assets/images/posts/firebase-auth-journey/hero.png)
17+
*The trials and errors encountered while implementing Firebase dual authentication system in Unity*
18+
19+
## 🤦‍♂️ It All Started with This Problem
20+
21+
**Problem**: How to allow guest users to save data in a game app while preserving their existing data when they sign up later?
22+
23+
**Solution**: Implement seamless user experience with Firebase Anonymous Authentication + Account Linking
24+
25+
Initially, I thought "Can't we just use device ID?" But I realized data would be lost when changing devices or reinstalling the app. Firebase Anonymous Auth was the answer.
26+
27+
```mermaid
28+
graph TD
29+
A[App Start] --> B{User Status}
30+
B -->|Guest| C[Firebase Anonymous Auth]
31+
B -->|Member| D[Firebase Email Auth]
32+
C --> E[Device-specific JWT Issuance]
33+
D --> F[Member JWT Issuance]
34+
E --> G{Sign-up Request?}
35+
G -->|Yes| H[Account Linking]
36+
G -->|No| I[Continue as Guest]
37+
H --> J[Existing Data + Member Conversion]
38+
```
39+
40+
## 💻 Core Implementation Code
41+
42+
### Firebase Anonymous Authentication (Unity)
43+
44+
```csharp
45+
// Initially, I only did this...
46+
FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync().ContinueWith(task => {
47+
if (task.IsCompletedSuccessfully) {
48+
FirebaseUser user = task.Result.User;
49+
Debug.Log("Anonymous login successful: " + user.UserId);
50+
}
51+
});
52+
53+
// Actually need to get ID Token for server verification
54+
private async void AuthenticateAnonymously() {
55+
try {
56+
var result = await FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync();
57+
var idToken = await result.User.GetIdTokenAsync(false);
58+
59+
// Send ID Token to server
60+
await SendDeviceAuthRequest(idToken);
61+
} catch (Exception e) {
62+
Debug.LogError($"Anonymous authentication failed: {e.Message}");
63+
}
64+
}
65+
```
66+
67+
### Account Linking Implementation (The most challenging part)
68+
69+
```csharp
70+
// Initially didn't understand why this wasn't working
71+
private async void LinkWithEmail(string email, string password) {
72+
try {
73+
var credential = EmailAuthProvider.GetCredential(email, password);
74+
75+
// Key: Link email account to current anonymous user
76+
var result = await FirebaseAuth.DefaultInstance.CurrentUser
77+
.LinkWithCredentialAsync(credential);
78+
79+
// Notify server with new ID Token
80+
var newIdToken = await result.User.GetIdTokenAsync(false);
81+
await SendLoginRequest(newIdToken);
82+
83+
Debug.Log("Account Linking successful!");
84+
} catch (FirebaseException e) {
85+
if (e.ErrorCode == AuthError.EmailAlreadyInUse) {
86+
Debug.LogError("Email is already in use");
87+
}
88+
}
89+
}
90+
```
91+
92+
### Server-side Processing (AWS Lambda)
93+
94+
```javascript
95+
// User processing after Firebase ID Token verification
96+
exports.handler = async (event) => {
97+
try {
98+
const { idToken } = JSON.parse(event.body);
99+
100+
// Verify token with Firebase Admin SDK
101+
const decodedToken = await admin.auth().verifyIdToken(idToken);
102+
const { uid, email, firebase } = decodedToken;
103+
104+
// Query existing user from DynamoDB
105+
const existingUser = await getUserByUID(uid);
106+
107+
if (existingUser) {
108+
// Account Linking: Anonymous → Member conversion
109+
if (!existingUser.email && email) {
110+
await updateUserToMember(uid, email);
111+
return {
112+
success: true,
113+
isUpgrade: true,
114+
message: "Can continue using existing JWT token"
115+
};
116+
}
117+
} else {
118+
// Create new user
119+
await createNewUser(uid, email || null);
120+
}
121+
122+
// Issue JWT (no distinction between anonymous/member)
123+
const jwt = generateJWT({ uid, email, type: email ? 'user' : 'anonymous' });
124+
125+
return { success: true, jwt, isNewUser: !existingUser };
126+
} catch (error) {
127+
return { success: false, error: error.message };
128+
}
129+
};
130+
```
131+
132+
## 🔧 Lessons Learned from Trial and Error
133+
134+
### 1. Importance of Unified JWT Secret
135+
Initially, I tried to create separate JWT secrets for anonymous and member users. This caused users to be logged out when Account Linking invalidated existing tokens.
136+
137+
**Solution**: Use single JWT secret to ensure session continuity during mode transitions
138+
139+
### 2. Firebase ID Token Expiration Handling
140+
Firebase ID Tokens expire every hour. I didn't know this initially and wondered "Why does authentication suddenly fail?"
141+
142+
**Solution**: Firebase SDK automatically refreshes tokens, so no additional client-side handling needed
143+
144+
### 3. DynamoDB User Data Structure
145+
```json
146+
{
147+
"uid": "firebase_uid_here",
148+
"type": "anonymous", // or "user"
149+
"email": null, // Updated during Account Linking
150+
"createdAt": "2025-06-21T10:00:00Z",
151+
"lastLoginAt": "2025-06-21T15:30:00Z",
152+
"learningData": { /* Game progress data */ }
153+
}
154+
```
155+
156+
**Key Point**: During Account Linking, only update `type` and `email` while preserving `learningData`
157+
158+
### 4. Importance of Network Error Handling
159+
High dependency on Firebase makes the system sensitive to network issues. Offline scenarios must also be considered.
160+
161+
```csharp
162+
// Including retry logic
163+
private async Task<string> GetIdTokenWithRetry(int maxRetries = 3) {
164+
for (int i = 0; i < maxRetries; i++) {
165+
try {
166+
return await FirebaseAuth.DefaultInstance.CurrentUser.GetIdTokenAsync(false);
167+
} catch (Exception e) {
168+
if (i == maxRetries - 1) throw;
169+
await Task.Delay(1000 * (i + 1)); // Exponential backoff
170+
}
171+
}
172+
return null;
173+
}
174+
```
175+
176+
## 💡 Results and Insights
177+
178+
### Achievements
179+
- **Perfect Data Continuity**: 100% data preservation during guest → member transition
180+
- **Seamless UX**: So natural that users don't notice mode transitions
181+
- **Scalable Architecture**: Social login additions possible with the same pattern
182+
183+
### Limitations
184+
- **Firebase Dependency**: Entire authentication system vulnerable to Firebase outages
185+
- **Token Management Complexity**: JWT expiration handling on client-side more challenging than expected
186+
187+
I plan to add OAuth social login using the same Account Linking pattern. Hope this helps anyone implementing similar systems, and please share better approaches in the comments if you know any! 🙏
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
layout: post
3+
title: "Unity + Firebase認証の苦労話:Anonymousからアカウントリンクまで"
4+
date: 2025-06-21 10:00:00 +0900
5+
categories: [Development, Unity]
6+
tags: [Unity, Firebase, Authentication, AccountLinking, AWS, Lambda, 認証システム, ゲーム開発]
7+
author: "Kevin Park"
8+
excerpt: "ゲストユーザーも会員も自然に!Firebase Anonymous Authからアカウントリンクまでの実装過程の試行錯誤と解決策"
9+
image: "/assets/images/posts/firebase-auth-journey/hero.png"
10+
mermaid: true
11+
lang: ja
12+
---
13+
14+
# Unity + Firebase認証の苦労話:Anonymousからアカウントリンクまで
15+
16+
![Unity Firebase Authentication System](/assets/images/posts/firebase-auth-journey/hero.png)
17+
*UnityでFirebase二重認証システムを実装する際に経験した試行錯誤*
18+
19+
## 🤦‍♂️ こんな悩みから始まった
20+
21+
**問題**: ゲームアプリでゲストユーザーもデータを保存し、後から会員登録しても既存データを失わないようにするには?
22+
23+
**解決**: Firebase Anonymous Authentication + アカウントリンクでスムーズなユーザー体験を実装
24+
25+
最初は「デバイスIDを使えばいいんじゃない?」と思ったが、デバイス変更やアプリ再インストール時にデータが消失するのを見て気づいた。Firebase Anonymous Authが答えだった。
26+
27+
```mermaid
28+
graph TD
29+
A[アプリ開始] --> B{ユーザー状態}
30+
B -->|ゲスト| C[Firebase Anonymous Auth]
31+
B -->|会員| D[Firebase Email Auth]
32+
C --> E[デバイス別JWT発行]
33+
D --> F[会員JWT発行]
34+
E --> G{会員登録要求?}
35+
G -->|Yes| H[アカウントリンク]
36+
G -->|No| I[ゲストのまま継続]
37+
H --> J[既存データ + 会員転換]
38+
```
39+
40+
## 💻 核心実装コード
41+
42+
### Firebase Anonymous認証 (Unity)
43+
44+
```csharp
45+
// 最初はこれだけしかしなかった...
46+
FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync().ContinueWith(task => {
47+
if (task.IsCompletedSuccessfully) {
48+
FirebaseUser user = task.Result.User;
49+
Debug.Log("匿名ログイン成功: " + user.UserId);
50+
}
51+
});
52+
53+
// 実際にはID Tokenまで取得してサーバーで検証可能にする必要がある
54+
private async void AuthenticateAnonymously() {
55+
try {
56+
var result = await FirebaseAuth.DefaultInstance.SignInAnonymouslyAsync();
57+
var idToken = await result.User.GetIdTokenAsync(false);
58+
59+
// サーバーにID Token送信
60+
await SendDeviceAuthRequest(idToken);
61+
} catch (Exception e) {
62+
Debug.LogError($"匿名認証失敗: {e.Message}");
63+
}
64+
}
65+
```
66+
67+
### アカウントリンク実装 (最も苦労した部分)
68+
69+
```csharp
70+
// 最初はなぜこれが動かないのかわからなかった
71+
private async void LinkWithEmail(string email, string password) {
72+
try {
73+
var credential = EmailAuthProvider.GetCredential(email, password);
74+
75+
// キーポイント: 現在の匿名ユーザーにメールアカウントをリンク
76+
var result = await FirebaseAuth.DefaultInstance.CurrentUser
77+
.LinkWithCredentialAsync(credential);
78+
79+
// 新しいID Tokenでサーバーに通知
80+
var newIdToken = await result.User.GetIdTokenAsync(false);
81+
await SendLoginRequest(newIdToken);
82+
83+
Debug.Log("アカウントリンク成功!");
84+
} catch (FirebaseException e) {
85+
if (e.ErrorCode == AuthError.EmailAlreadyInUse) {
86+
Debug.LogError("既に使用中のメールアドレスです");
87+
}
88+
}
89+
}
90+
```
91+
92+
### サーバーサイド処理 (AWS Lambda)
93+
94+
```javascript
95+
// Firebase ID Token検証後のユーザー処理
96+
exports.handler = async (event) => {
97+
try {
98+
const { idToken } = JSON.parse(event.body);
99+
100+
// Firebase Admin SDKでトークン検証
101+
const decodedToken = await admin.auth().verifyIdToken(idToken);
102+
const { uid, email, firebase } = decodedToken;
103+
104+
// DynamoDBから既存ユーザー照会
105+
const existingUser = await getUserByUID(uid);
106+
107+
if (existingUser) {
108+
// アカウントリンク: 匿名 → 会員転換
109+
if (!existingUser.email && email) {
110+
await updateUserToMember(uid, email);
111+
return {
112+
success: true,
113+
isUpgrade: true,
114+
message: "既存JWTトークンで継続使用可能です"
115+
};
116+
}
117+
} else {
118+
// 新規ユーザー作成
119+
await createNewUser(uid, email || null);
120+
}
121+
122+
// JWT発行 (匿名/会員を区別しない)
123+
const jwt = generateJWT({ uid, email, type: email ? 'user' : 'anonymous' });
124+
125+
return { success: true, jwt, isNewUser: !existingUser };
126+
} catch (error) {
127+
return { success: false, error: error.message };
128+
}
129+
};
130+
```
131+
132+
## 🔧 試行錯誤の過程で学んだこと
133+
134+
### 1. JWT Secret統一の重要性
135+
最初は匿名用、会員用JWT Secretを別々に作ろうとした。アカウントリンク時に既存トークンが無効化されてユーザーがログアウトされる問題が発生した。
136+
137+
**解決**: 単一JWT Secret使用でモード切り替え時のセッション継続性を保証
138+
139+
### 2. Firebase ID Token有効期限処理
140+
Firebase ID Tokenは1時間ごとに期限切れになる。最初はこれを知らずに「なぜ急に認証できなくなるんだ?」と思った。
141+
142+
**解決**: Firebase SDKが自動で更新してくれるのでクライアントサイドで別途処理不要
143+
144+
### 3. DynamoDBユーザーデータ構造
145+
```json
146+
{
147+
"uid": "firebase_uid_here",
148+
"type": "anonymous", // または "user"
149+
"email": null, // アカウントリンク時に更新
150+
"createdAt": "2025-06-21T10:00:00Z",
151+
"lastLoginAt": "2025-06-21T15:30:00Z",
152+
"learningData": { /* ゲーム進行データ */ }
153+
}
154+
```
155+
156+
**キーポイント**: アカウントリンク時は`type``email`のみ更新し、`learningData`はそのまま維持
157+
158+
### 4. ネットワークエラー処理の重要性
159+
Firebase依存度が高い分、ネットワーク問題に敏感である。オフライン状況も考慮する必要がある。
160+
161+
```csharp
162+
// リトライロジック含む
163+
private async Task<string> GetIdTokenWithRetry(int maxRetries = 3) {
164+
for (int i = 0; i < maxRetries; i++) {
165+
try {
166+
return await FirebaseAuth.DefaultInstance.CurrentUser.GetIdTokenAsync(false);
167+
} catch (Exception e) {
168+
if (i == maxRetries - 1) throw;
169+
await Task.Delay(1000 * (i + 1)); // 指数バックオフ
170+
}
171+
}
172+
return null;
173+
}
174+
```
175+
176+
## 💡 結果と学んだ点
177+
178+
### 成果
179+
- **完璧なデータ継続性**: ゲスト → 会員転換時のデータ100%保存
180+
- **スムーズなUX**: ユーザーがモード切り替えを意識しないほど自然
181+
- **拡張可能な構造**: ソーシャルログイン追加も同じパターンで可能
182+
183+
### 惜しい点
184+
- **Firebase依存性**: Firebase障害時に認証システム全体が麻痺
185+
- **トークン管理の複雑性**: クライアントサイドでのJWT有効期限処理が思ったより面倒
186+
187+
今後はOAuthソーシャルログインも同じアカウントリンクパターンで追加予定である。同じようなシステムを実装される方々の参考になれば幸いで、より良い方法をご存知の方がいればコメントで共有してください!🙏

0 commit comments

Comments
 (0)