|
| 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 | + |
| 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! 🙏 |
0 commit comments