在使用 Firebase 的 REST API 测试我们的产品之一(Web 应用程序)的安全性时,我们惊讶地发现,在 Firebase 实现的 V3 中,刷新令牌永不过期,允许任何刷新令牌永远创建新的代币。
虽然本地存储今天看起来是一个相当安全的解决方案,但我们担心明天可能会失败,即使是很短的时间,并且我们无法阻止某人使用任何这些刷新令牌。
双因素身份验证将有助于缓解该问题,但第一步仍然会受到损害。
是否有一种方法可以使用 Firebase 将代币列入黑名单或类似行为,而无需我们自己处理所有代币交换(例如铸造)?我们在查看文档时找不到此类功能。
任何建议表示赞赏。
身份验证会话不会因 Firebase 登录而过期。但 ID 令牌必须每小时刷新一次,才能保持对服务的访问。如果您禁用帐户,刷新令牌将失败,并且该帐户将无法再访问服务。无法使单个令牌失效。
revokeRefreshTokens()
。 虽然这不会让你杀死无效的 JWT,但它确实允许你阻止刷新令牌(至少从我到目前为止的测试来看),并且它允许更清晰的控制Firebase 数据库内部的流动。
想为这个问题添加一个具体的例子。这就是我使用上下文提供程序/他们处理全局状态的方式使用 SolidJS 处理它的方式:
我的
routes.tsx
看起来像这样:
import { lazy } from 'solid-js';
import { Route, Router } from '@solidjs/router';
import { getCurrentUser } from '~/apis/current-user.api';
import AuthContextProvider from '~/contexts/Auth.context';
import HomeRoutes from '~/pages/auth/routes';
const LandingPage = lazy(() => import('./pages/landing/index'));
const user = await getCurrentUser();
export const Routes = () => {
return (<AuthContextProvider
user={user}>
<Router>
<Route path='/'>
<Route path='/' component={LandingPage} />,
<HomeRoutes/>,
<Route path='/*' component={LandingPage} />,
</Route>,
</Router>
</AuthContextProvider>)
};
我正在获取当前用户并将其传递给我的
AuthContextProvider
,因为它位于所有路由的顶层,每个组件都可以访问它,并且能够调用 const { user } = useAuthContext();
来获取用户信息。
~/apis/current-user.api 看起来像这样:
import { API_URL, COOKIES, getHeadersWithAuth } from "~/constants/api";
import { emptyUser, User } from "~/models/user.model";
import { firebaseAuth } from '~/utils/firebase';
const TOKEN_EXPIRATION_TIME = 55 * 60 * 1000; // 55 minutes in milliseconds
export const refreshUserToken = async () => {
const userToken = localStorage.getItem("userToken");
const tokenCreationTime = parseInt(localStorage.getItem("tokenCreationTime"), 10);
const currentTime = Date.now();
if (userToken && tokenCreationTime && currentTime - tokenCreationTime < TOKEN_EXPIRATION_TIME) {
sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);
return true;
}
try {
// Force a token refresh
const user = firebaseAuth.currentUser;
if (!user) {
return false;
}
const userToken = await user.getIdToken(true);
sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);
// Update the local storage with new token and creation time
localStorage.setItem("userToken", userToken);
localStorage.setItem("tokenCreationTime", Date.now().toString());
return true;
} catch (error) {
return false;
}
};
export const getCurrentUser = async () => {
const result = await refreshUserToken();
if (!result) {
return emptyUser();
}
if (!sessionStorage.getItem(COOKIES.ACCESS_TOKEN)) {
return emptyUser();
}
const endPoint = `${API_URL}/auth/current-user`;
const res = await fetch(endPoint, {
method: 'GET',
headers: getHeadersWithAuth(),
// credentials: 'include',
}).catch(err => {
console.error(`Error: getCurrentUser - ${endPoint}`, err);
return err;
});
if (res.status !== 200) {
sessionStorage.removeItem(COOKIES.ACCESS_TOKEN);
// Remove tokens from local storage
localStorage.removeItem("userToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("tokenCreationTime");
return emptyUser();
}
type data = {
user: User;
}
const { user: _user }: data = await res.json();
const user = _user ? new User(_user) : emptyUser();
return user;
}
这样,每当客户端刷新或转到新页面时,都会调用
getCurrentUser
函数并检查令牌是否已过期。如果没有,它将使用会话令牌中找到的内容。如果没有,它将创建一个新的。
我正在使用本地存储,以便用户即使在关闭窗口并返回时也保持登录状态。我使用
setPersistence
和 browserLocalPersistence
选项来做到这一点。
当用户注销时,我将从本地存储中删除数据并调用
revokeRefreshTokens
,这会导致refreshToken不再有效。
如果您愿意,您当然可以更进一步,使事情变得更加安全,但这应该是一个很好的起点。