Introduction.
The following error occurred when the user clicked the button to unsubscribe from the application, although the user’s unsubscribe flow was created in the application with the talking cat-shaped robot “Mia”.
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: [firebase_auth/requires-recent-login] This operation is sensitive and requires recent authentication. Log in again before retrying this request.
#0 FirebaseAuthUserHostApi.delete (package:firebase_auth_platform_interface/src/pigeon/messages.pigeon.dart:1581:7)
#1 MethodChannelUser.delete (package:firebase_auth_platform_interface/src/method_channel/method_channel_user.dart:35:7)
#2 UserNotifier.deleteUser (package:clocky_app/api/user_notifier.dart:31:7)
Describes how to handle “requires-recent-login” errors when processing user withdrawal using Firebase Auth.
What is the problem? Occurs when there is no recent login
Here is the code for Flutter’s current user withdrawal process.
// lib/api/user_notifier.dart
class UserNotifier extends StateNotifier {
final ApiClient apiClient;
UserNotifier(this.apiClient) : super(null);
Future deleteUser() async {
// DBのデータを削除
await apiClient.deleteUser(state?.uid ?? '');
// Firebase Authデータを削除
auth.User? currentUser = auth.FirebaseAuth.instance.currentUser;
if (currentUser != null) {
await currentUser.delete();
}
state = null;
}
}
Checking the definition of the delete method of the FirebaseAuth class, the following warning was noted.
/// Deletes and signs out the user.
///
/// **Important**: this is a security-sensitive operation that requires the
/// user to have recently signed in. If this requirement isn't met, ask the
/// user to authenticate again and then call [User.reauthenticateWithCredential].
///
/// A [FirebaseAuthException] maybe thrown with the following error code:
/// - **requires-recent-login**:
/// - Thrown if the user's last sign-in time does not meet the security
/// threshold. Use [User.reauthenticateWithCredential] to resolve. This
/// does not apply if the user is anonymous.
Future delete() async {
return _delegate.delete();
}
This error occurs when a user has not logged in recently for security reasons. The user must be reauthenticated before being dismissed.
Incidentally, the Firebase documentation did not specify, as far as I could tell, how many days of inactivity would cause this error to appear.
Also, although not directly related to this issue, user data deletion in the DB had been performed prior to Firebase Auth data deletion, so when the “Unsubscribe” button was pressed, user data was deleted from the DB, but the Firebase Auth data remained undeleted This phenomenon was occurring, so it is necessary to recover this data as well.
Change the Firebase Auth deactivation process to use the Firebase Admin SDK on the Go server side instead of the Flutter front end. The SDK will automatically manage token expiration dates and update tokens as needed, thus separating the management of authentication status on the client side and eliminating the re-authentication error problem.
Go: Implement Firebase Auth Unsubscribe Process
Add Firebase Admin SDK to server
The Firebase Admin SDK is a set of server libraries used to operate Firebase from a privileged environment. Add the SDK by executing the following
https://github.com/firebase/firebase-admin-go
# Install the latest version:
go install firebase.google.com/go/v4@latest
# Or install a specific version:
go install firebase.google.com/go/v4@4.13.0
Initialize SDK with OAuth 2.0 update token
Import Firebase Go SDK and use the GoogleApplicationCredentials stored in the Config structure to set options for credentials.
Then, an instance of the Firebase application is created with the configured authentication information as an argument.
This SDK initialization accesses Firebase Authentication through the generated *firebase.App
instance to remove authentication data for a specific user at the time of the withdrawal request.
// firebase_auth.go
package clocky_be
import (
"context"
firebase "firebase.google.com/go"
"google.golang.org/api/option"
)
func FirebaseApp(config *Config) *firebase.App {
opt := option.WithCredentialsJSON([]byte(config.GoogleApplicationCredentials))
app, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
panic("failed to initialize firebase app")
}
return app
}
Add Firebase Auth user deletion: Firebase Auth -> DB
Incorporate the process of deleting users from Firebase Auth into the existing HandleDeleteUser
method that deletes users from the DB during the withdrawal process.
Deleting Firebase Auth data is the most important and risky operation in the withdrawal process. This is because once a Firebase Auth account is deleted, its information cannot be reverted. Therefore, the user data deletion process should be changed in the following order
- First, delete Firebase Auth user data.
- Next, delete the user data in the database.
Add a new *firebase.App
field to the UserHandler structure.
Create a new instance of type UserHandler
using the NewUserHandler
function and access the Firebase Admin SDK functions through the app
field of that instance to remove a specific user from Firebase Auth.
// user_handler.go
type UserHandler struct {
db *sqlx.DB
sm ShadowManager
app *firebase.App
}
func NewUserHandler(db *sqlx.DB, sm ShadowManager, app *firebase.App) *UserHandler {
return &UserHandler{
db: db,
sm: sm,
app: app,
}
}
func (h *UserHandler) HandleDeleteUser(c echo.Context) error {
uid := c.Get("uid").(string)
if uid == "" {
return echo.NewHTTPError(http.StatusBadRequest, "UID is required")
}
fmt.Printf("Want to delete uid: %vn", uid)
// Firebase Authからユーザーを先に削除
authClient, err := h.app.Auth(context.Background())
if err != nil {
c.Logger().Errorf("Failed to get Firebase Auth client: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get Firebase Auth client")
}
if err := authClient.DeleteUser(context.Background(), uid); err != nil {
c.Logger().Errorf("Failed to delete Firebase auth user: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete Firebase auth user")
}
// Firebase削除に成功したらデータベースからユーザーを削除
if err := DeleteUser(h.db, uid); err != nil {
c.Logger().Errorf("failed to delete user from DB: %v", err)
}
return c.NoContent(http.StatusNoContent)
}
Flutter: change to only sending delete requests to API
Since the server side now uses the Firebase Admin SDK to handle user deletion, the deleteUser
method on the Flutter side needs to be modified to not handle user deletion from Firebase Auth.
On the Flutter side, it is sufficient to send a delete request to the API, as the backend handles all the deletion processing.
API client (api_client.dart)
Modify the deleteUser
function in api_client.dart
so that the user ID is no longer required when sending a delete request to the server side. Instead, include an authentication token in the header to identify the user on the server side.
// lib/api/api_client.dart
Future deleteUser() async {
final url = Uri.parse('$apiUrl/app/me');
final headers = await apiHeaders(); // ここでユーザートークンを含むヘッダーを取得
print("apiHeaders: $headers");
final response = await http.delete(
url,
headers: headers,
);
if (response.statusCode != 204) {
throw Exception('Failed to delete user: ${response.body}');
}
}
User Notifier: Request user deletion through API client
In user_notifier.dart
, remove the user removal process from Firebase Auth and simply request user removal through the API client.
// lib/api/user_notifier.dart
Future deleteUser() async {
try {
await apiClient.deleteUser(); // APIクライアントを通じてユーザー削除をリクエスト
state = null; // 状態を更新
print("User delete request sent successfully");
} catch (e) {
print('Failed to delete user: $e');
throw e; // エラーを再スローして、上位の処理でキャッチ可能に
}
}
Create withdrawal completion screen
Call userNotifier.deleteUser()
to perform the unsubscribe process, and after the unsubscribe process is successful, use Navigator.pushReplacement
to transition the user to the WithdrawalCompleteScreen()
. This allows the withdrawal process
// lib/screens/home/customer_support_screen.dart
//ユーザー削除ダイアログ
Future _showWithdrawDialog(BuildContext context) async {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('退会'),
content: const Text('本当に退会しますか?この操作は取り消せません。'),
actions: [
TextButton(
child: const Text('キャンセル'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
Consumer(builder: (context, ref, _) {
final userNotifier = ref.read(userProvider.notifier);
return TextButton(
child: const Text('退会する'),
onPressed: () async {
userNotifier.deleteUser().then((value) async {
final wifiSetupNotifier =
ref.read(wifiSetupCheckProvider.notifier);
await wifiSetupNotifier.deleteWifiSetupDone();
if (context.mounted) Navigator.of(dialogContext).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => WithdrawalCompleteScreen()));
});
},
);
})
],
);
},
);
}
Inform the customer that the withdrawal has been successfully completed on the withdrawal processing screen.
// lib/screens/home/withdrawal_complete_screen.dart
import 'package:clocky_app/screens/registration/start_screen.dart';
import 'package:flutter/material.dart';
class WithdrawalCompleteScreen extends StatelessWidget {
const WithdrawalCompleteScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('退会完了'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('退会手続きが完了しました。'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => StartScreen())),
child: const Text('新規登録へ戻る'),
),
],
),
),
);
}
}
operation check
When a user sends an unsubscribe request from the app, a Firebase ID token is passed to the server side as the value of the Authorization
header, which is used for user authentication.
flutter: apiHeaders: {Content-Type: application/json, Accept: application/json, Authorization: Bearer XXXXXX}
flutter: User delete request sent successfully
The server processes the user deletion based on a specific user ID (XXXXXXXX), succeeds, and returns HTTP status code 204.
clocky_api_local | Want to delete uid: XXXXXXX
clocky_api_local | {"time":"2024-05-07T07:27:35.548105276Z","id":"","remote_ip":"192.168.65.1","host":"192.168.XX.XXX:8080","method":"DELETE","uri":"/app/me","user_agent":"Dart/3.3 (dart:io)","status":204,"error":"","latency":1113168250,"latency_human":"1.11316825s","bytes_in":0,"bytes_out":0}
In fact, we confirmed that the user in question was removed from Firebase Auth and from the DB.
The application screen also confirmed that the user can actually go to the new registration screen by clicking on the button that takes the user to the screen for completing the withdrawal and then back to the new registration.
コメント