已有用户邮箱时,Firebase无密码登录如何跳过邮箱二次输入?安全风险与实现方案咨询
Great question—you’re right to avoid both repeating the email prompt and exposing sensitive data in plaintext. Let’s break down two secure approaches to solve this, tailored to your existing Firebase + AngularJS setup.
Approach 1: Encrypt the Email and Pass It as a URL Parameter
This method hides the email in plain sight by encrypting it before adding it to the login URL, then decrypting it on the frontend.
Backend (Python) Modifications
First, add encryption logic (we’ll use cryptography’s Fernet, a secure symmetric encryption standard):
import firebase_admin from firebase_admin import auth from google.cloud import firestore from cryptography.fernet import Fernet import os # Store this key in an environment variable (never hardcode it!) # Generate a key once with Fernet.generate_key() ENCRYPTION_KEY = os.environ.get("FIREBASE_EMAIL_ENCRYPTION_KEY").encode() cipher_suite = Fernet(ENCRYPTION_KEY) # ... [your existing create_new_auth and create_new_pre_user functions] ... def main(dictionary): firebase_admin.initialize_app() db = firestore.Client() uid = create_new_auth(dictionary) create_new_pre_user(db, dictionary, uid) # Encrypt the user's email encrypted_email = cipher_suite.encrypt(dictionary['email'].encode()).decode() action_code_settings = auth.ActionCodeSettings( url=f'http://localhost:4200/login?e={encrypted_email}', handle_code_in_app=True, ios_bundle_id='com.example.ios', android_package_name='com.example.android', android_install_app=True, android_minimum_version='12', dynamic_link_domain='magic42.page.link', ) link = auth.generate_sign_in_with_email_link(dictionary['email'], action_code_settings) print(f"Generated login link: {link}")
Frontend (AngularJS) Modifications
Install the fernet-js package (npm install fernet-js) and update your login logic to decrypt the email:
import { Component } from '@angular/core'; import { AuthService } from './auth.service'; import { Router } from '@angular/router'; import Fernet from 'fernet-js'; // Fetch this key from your environment variables (match the backend key!) const ENCRYPTION_KEY = import.meta.env.FIREBASE_EMAIL_ENCRYPTION_KEY; const cipherSuite = new Fernet(ENCRYPTION_KEY); @Component({ /* ... */ }) export class LoginComponent { constructor(private authService: AuthService, private router: Router) {} private signInWithEmail() { if (this.authService.isSignInWithEmailLink(window.location.href)) { let email: string | null = null; const urlParams = new URLSearchParams(window.location.search); const encryptedEmail = urlParams.get('e'); if (encryptedEmail) { try { // Decrypt the email email = cipherSuite.decrypt(encryptedEmail).toString(); } catch (err) { console.error("Failed to decrypt email:", err); } } // Fallback to prompt if decryption fails or parameter is missing if (!email) { email = window.prompt('Please provide your email for confirmation'); } if (email) { this.authService.signInWithEmailLink(email, window.location.href) .then((result) => { window.localStorage.removeItem('emailForSignIn'); this.router.navigate(['/patient', 'quiz']); }) .catch((error) => { console.error("Login failed:", error); }); } } } }
Approach 2: Pass UID via Dynamic Link, Fetch Email from Firestore
This leverages Firebase’s Dynamic Links and Firestore to retrieve the email without encrypting anything. Since UIDs are non-sensitive public identifiers in Firebase, this is a clean, Firebase-native approach.
Backend (Python) Modifications
Add the user’s UID as a parameter to your login URL:
def main(dictionary): firebase_admin.initialize_app() db = firestore.Client() uid = create_new_auth(dictionary) create_new_pre_user(db, dictionary, uid) action_code_settings = auth.ActionCodeSettings( url=f'http://localhost:4200/login?uid={uid}', handle_code_in_app=True, ios_bundle_id='com.example.ios', android_package_name='com.example.android', android_install_app=True, android_minimum_version='12', dynamic_link_domain='magic42.page.link', ) link = auth.generate_sign_in_with_email_link(dictionary['email'], action_code_settings) print(f"Generated login link: {link}")
Frontend (AngularJS) Modifications
Use Firebase’s Dynamic Links SDK to parse the UID, then fetch the email from Firestore:
import { Component } from '@angular/core'; import { AuthService } from './auth.service'; import { Router } from '@angular/router'; import { AngularFirestore } from '@angular/fire/firestore'; import { getDynamicLinks, parseDynamicLink } from "@firebase/dynamic-links"; import { AngularFireAuth } from '@angular/fire/auth'; @Component({ /* ... */ }) export class LoginComponent { constructor( private authService: AuthService, private router: Router, private afs: AngularFirestore, private afAuth: AngularFireAuth ) {} private async signInWithEmail() { if (this.authService.isSignInWithEmailLink(window.location.href)) { let email: string | null = null; try { // Parse the dynamic link to get the UID const dynamicLink = await parseDynamicLink( getDynamicLinks(this.afAuth.app), window.location.href ); const uid = dynamicLink.url.searchParams.get('uid'); if (uid) { // Fetch the user's email from Firestore const userDoc = await this.afs.collection('users').doc(uid).get().toPromise(); if (userDoc?.exists) { email = userDoc.data()['email']; } } } catch (err) { console.error("Failed to retrieve user data:", err); } // Fallback to prompt if fetch fails if (!email) { email = window.prompt('Please provide your email for confirmation'); } if (email) { this.authService.signInWithEmailLink(email, window.location.href) .then((result) => { window.localStorage.removeItem('emailForSignIn'); this.router.navigate(['/patient', 'quiz']); }) .catch((error) => { console.error("Login failed:", error); }); } } } }
Security Considerations
- Approach 1: Keep your encryption key secure (use environment variables or a secret manager like Google Cloud Secret Manager). Even if the encrypted email is intercepted, attackers can’t decrypt it without the key—and they still need the valid action code from the login link to authenticate.
- Approach 2: UIDs are public, but only users with a valid login link can use the UID to fetch the email and complete authentication. Ensure your Firestore rules allow read access to user documents for this initial step (you can tighten rules once the user is logged in).
Both methods eliminate the need for users to re-enter their email while avoiding plaintext exposure. Approach 2 is generally preferred since it fits seamlessly into Firebase’s ecosystem.
内容的提问来源于stack exchange,提问作者cuzureau

