Firebase 驗證提供傳統型的伺服器端工作階段 Cookie 管理 仰賴工作階段 Cookie 的網站相較於用於用戶端的短效 ID 權杖,這個解決方案有幾項優點,因為後者每次可能都需要重新導向機制,才能在會話 Cookie 到期時更新。
- 針對只能透過已授權服務帳戶產生的 JWT 工作階段權杖,提高安全性。
- 無狀態工作階段 Cookie 隨附使用 JWT 進行驗證的所有好處。工作階段 Cookie 與 ID 權杖具有相同的聲明 (包括自訂憑證附加資訊),因此相同的權限檢查亦能對工作階段 Cookie 強制執行。
- 可建立工作階段 Cookie,自訂到期時間介於 5 分鐘到 2 週之間。
- 可根據應用程式需求靈活執行 Cookie 政策:網域、路徑、安全、
httpOnly
等 - 在懷疑權杖遭竊時,使用現有的更新權杖撤銷 API 撤銷工作階段 Cookie。
- 偵測重大帳戶變更時,能否偵測到工作階段撤銷。
登入
假設應用程式使用 httpOnly
伺服器端 Cookie,請使用用戶端 SDK 在登入頁面上登入使用者。系統會產生 Firebase ID 權杖
系統會透過 HTTP POST 將 ID 權杖傳送至工作階段登入端點,其中
Admin SDK 會產生工作階段 Cookie成功後,狀態應該
而已從用戶端儲存空間清除
firebase.initializeApp({
apiKey: 'AIza…',
authDomain: '<PROJECT_ID>.firebasepp.com'
});
// As httpOnly cookies are to be used, do not persist any state client side.
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
// When the user signs in with email and password.
firebase.auth().signInWithEmailAndPassword('user@example.com', 'password').then(user => {
// Get the user's ID token as it is needed to exchange for a session cookie.
return user.getIdToken().then(idToken = > {
// Session login endpoint is queried and the session cookie is set.
// CSRF protection should be taken into account.
// ...
const csrfToken = getCookie('csrfToken')
return postIdTokenToSessionLogin('/sessionLogin', idToken, csrfToken);
});
}).then(() => {
// A page redirect would suffice as the persistence is set to NONE.
return firebase.auth().signOut();
}).then(() => {
window.location.assign('/profile');
});
建立執行階段 Cookie
為了用提供的 ID 權杖換取工作階段 Cookie, 必須提供端點將權杖傳送至端點,並使用 Firebase Admin SDK 設定自訂的工作階段時間長度。應採取適當措施 以防止跨網站要求偽造 (CSRF) 攻擊。
Node.js
app.post('/sessionLogin', (req, res) => {
// Get the ID token passed and the CSRF token.
const idToken = req.body.idToken.toString();
const csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (csrfToken !== req.cookies.csrfToken) {
res.status(401).send('UNAUTHORIZED REQUEST!');
return;
}
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
getAuth()
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
// Set cookie policy for session cookie.
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({ status: 'success' }));
},
(error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
}
);
});
Java
@POST
@Path("/sessionLogin")
@Consumes("application/json")
public Response createSessionCookie(LoginRequest request) {
// Get the ID token sent by the client
String idToken = request.getIdToken();
// Set session expiration to 5 days.
long expiresIn = TimeUnit.DAYS.toMillis(5);
SessionCookieOptions options = SessionCookieOptions.builder()
.setExpiresIn(expiresIn)
.build();
try {
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options);
// Set cookie policy parameters as required.
NewCookie cookie = new NewCookie("session", sessionCookie /* ... other parameters */);
return Response.ok().cookie(cookie).build();
} catch (FirebaseAuthException e) {
return Response.status(Status.UNAUTHORIZED).entity("Failed to create a session cookie")
.build();
}
}
Python
@app.route('/sessionLogin', methods=['POST'])
def session_login():
# Get the ID token sent by the client
id_token = flask.request.json['idToken']
# Set session expiration to 5 days.
expires_in = datetime.timedelta(days=5)
try:
# Create the session cookie. This will also verify the ID token in the process.
# The session cookie will have the same claims as the ID token.
session_cookie = auth.create_session_cookie(id_token, expires_in=expires_in)
response = flask.jsonify({'status': 'success'})
# Set cookie policy for session cookie.
expires = datetime.datetime.now() + expires_in
response.set_cookie(
'session', session_cookie, expires=expires, httponly=True, secure=True)
return response
except exceptions.FirebaseError:
return flask.abort(401, 'Failed to create a session cookie')
Go
return func(w http.ResponseWriter, r *http.Request) {
// Get the ID token sent by the client
defer r.Body.Close()
idToken, err := getIDTokenFromBody(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Set session expiration to 5 days.
expiresIn := time.Hour * 24 * 5
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
cookie, err := client.SessionCookie(r.Context(), idToken, expiresIn)
if err != nil {
http.Error(w, "Failed to create a session cookie", http.StatusInternalServerError)
return
}
// Set cookie policy for session cookie.
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: cookie,
MaxAge: int(expiresIn.Seconds()),
HttpOnly: true,
Secure: true,
})
w.Write([]byte(`{"status": "success"}`))
}
C#
// POST: /sessionLogin
[HttpPost]
public async Task<ActionResult> Login([FromBody] LoginRequest request)
{
// Set session expiration to 5 days.
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromDays(5),
};
try
{
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
var sessionCookie = await FirebaseAuth.DefaultInstance
.CreateSessionCookieAsync(request.IdToken, options);
// Set cookie policy parameters as required.
var cookieOptions = new CookieOptions()
{
Expires = DateTimeOffset.UtcNow.Add(options.ExpiresIn),
HttpOnly = true,
Secure = true,
};
this.Response.Cookies.Append("session", sessionCookie, cookieOptions);
return this.Ok();
}
catch (FirebaseAuthException)
{
return this.Unauthorized("Failed to create a session cookie");
}
}
對於敏感應用程式,應在發出工作階段 Cookie 之前檢查 auth_time
,盡可能縮短 ID 權杖遭竊時的攻擊時間:
Node.js
getAuth()
.verifyIdToken(idToken)
.then((decodedIdToken) => {
// Only process if the user just signed in in the last 5 minutes.
if (new Date().getTime() / 1000 - decodedIdToken.auth_time < 5 * 60) {
// Create session cookie and set it.
return getAuth().createSessionCookie(idToken, { expiresIn });
}
// A user that was not recently signed in is trying to set a session cookie.
// To guard against ID token theft, require re-authentication.
res.status(401).send('Recent sign in required!');
});
Java
// To ensure that cookies are set only on recently signed in users, check auth_time in
// ID token before creating a cookie.
FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
long authTimeMillis = TimeUnit.SECONDS.toMillis(
(long) decodedToken.getClaims().get("auth_time"));
// Only process if the user signed in within the last 5 minutes.
if (System.currentTimeMillis() - authTimeMillis < TimeUnit.MINUTES.toMillis(5)) {
long expiresIn = TimeUnit.DAYS.toMillis(5);
SessionCookieOptions options = SessionCookieOptions.builder()
.setExpiresIn(expiresIn)
.build();
String sessionCookie = FirebaseAuth.getInstance().createSessionCookie(idToken, options);
// Set cookie policy parameters as required.
NewCookie cookie = new NewCookie("session", sessionCookie);
return Response.ok().cookie(cookie).build();
}
// User did not sign in recently. To guard against ID token theft, require
// re-authentication.
return Response.status(Status.UNAUTHORIZED).entity("Recent sign in required").build();
Python
# To ensure that cookies are set only on recently signed in users, check auth_time in
# ID token before creating a cookie.
try:
decoded_claims = auth.verify_id_token(id_token)
# Only process if the user signed in within the last 5 minutes.
if time.time() - decoded_claims['auth_time'] < 5 * 60:
expires_in = datetime.timedelta(days=5)
expires = datetime.datetime.now() + expires_in
session_cookie = auth.create_session_cookie(id_token, expires_in=expires_in)
response = flask.jsonify({'status': 'success'})
response.set_cookie(
'session', session_cookie, expires=expires, httponly=True, secure=True)
return response
# User did not sign in recently. To guard against ID token theft, require
# re-authentication.
return flask.abort(401, 'Recent sign in required')
except auth.InvalidIdTokenError:
return flask.abort(401, 'Invalid ID token')
except exceptions.FirebaseError:
return flask.abort(401, 'Failed to create a session cookie')
Go
return func(w http.ResponseWriter, r *http.Request) {
// Get the ID token sent by the client
defer r.Body.Close()
idToken, err := getIDTokenFromBody(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
decoded, err := client.VerifyIDToken(r.Context(), idToken)
if err != nil {
http.Error(w, "Invalid ID token", http.StatusUnauthorized)
return
}
// Return error if the sign-in is older than 5 minutes.
if time.Now().Unix()-decoded.Claims["auth_time"].(int64) > 5*60 {
http.Error(w, "Recent sign-in required", http.StatusUnauthorized)
return
}
expiresIn := time.Hour * 24 * 5
cookie, err := client.SessionCookie(r.Context(), idToken, expiresIn)
if err != nil {
http.Error(w, "Failed to create a session cookie", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: cookie,
MaxAge: int(expiresIn.Seconds()),
HttpOnly: true,
Secure: true,
})
w.Write([]byte(`{"status": "success"}`))
}
C#
// To ensure that cookies are set only on recently signed in users, check auth_time in
// ID token before creating a cookie.
var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
var authTime = new DateTime(1970, 1, 1).AddSeconds(
(long)decodedToken.Claims["auth_time"]);
// Only process if the user signed in within the last 5 minutes.
if (DateTime.UtcNow - authTime < TimeSpan.FromMinutes(5))
{
var options = new SessionCookieOptions()
{
ExpiresIn = TimeSpan.FromDays(5),
};
var sessionCookie = await FirebaseAuth.DefaultInstance.CreateSessionCookieAsync(
idToken, options);
// Set cookie policy parameters as required.
this.Response.Cookies.Append("session", sessionCookie);
return this.Ok();
}
// User did not sign in recently. To guard against ID token theft, require
// re-authentication.
return this.Unauthorized("Recent sign in required");
驗證工作階段 Cookie 並檢查權限
登入後,網站的所有受限制存取權的部分都應檢查並驗證會話 Cookie,然後才根據某些安全性規則放送受限制的內容。
Node.js
// Whenever a user is accessing restricted content that requires authentication.
app.post('/profile', (req, res) => {
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
getAuth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
serveContentForUser('/profile', req, res, decodedClaims);
})
.catch((error) => {
// Session cookie is unavailable or invalid. Force user to login.
res.redirect('/login');
});
});
Java
@POST
@Path("/profile")
public Response verifySessionCookie(@CookieParam("session") Cookie cookie) {
String sessionCookie = cookie.getValue();
try {
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
final boolean checkRevoked = true;
FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(
sessionCookie, checkRevoked);
return serveContentForUser(decodedToken);
} catch (FirebaseAuthException e) {
// Session cookie is unavailable, invalid or revoked. Force user to login.
return Response.temporaryRedirect(URI.create("/login")).build();
}
}
Python
@app.route('/profile', methods=['POST'])
def access_restricted_content():
session_cookie = flask.request.cookies.get('session')
if not session_cookie:
# Session cookie is unavailable. Force user to login.
return flask.redirect('/login')
# Verify the session cookie. In this case an additional check is added to detect
# if the user's Firebase session was revoked, user deleted/disabled, etc.
try:
decoded_claims = auth.verify_session_cookie(session_cookie, check_revoked=True)
return serve_content_for_user(decoded_claims)
except auth.InvalidSessionCookieError:
# Session cookie is invalid, expired or revoked. Force user to login.
return flask.redirect('/login')
Go
return func(w http.ResponseWriter, r *http.Request) {
// Get the ID token sent by the client
cookie, err := r.Cookie("session")
if err != nil {
// Session cookie is unavailable. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
decoded, err := client.VerifySessionCookieAndCheckRevoked(r.Context(), cookie.Value)
if err != nil {
// Session cookie is invalid. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
serveContentForUser(w, r, decoded)
}
C#
// POST: /profile
[HttpPost]
public async Task<ActionResult> Profile()
{
var sessionCookie = this.Request.Cookies["session"];
if (string.IsNullOrEmpty(sessionCookie))
{
// Session cookie is not available. Force user to login.
return this.Redirect("/login");
}
try
{
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
var checkRevoked = true;
var decodedToken = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(
sessionCookie, checkRevoked);
return ViewContentForUser(decodedToken);
}
catch (FirebaseAuthException)
{
// Session cookie is invalid or revoked. Force user to login.
return this.Redirect("/login");
}
}
使用 Admin SDK verifySessionCookie API 驗證工作階段 Cookie。這是 減輕負擔系統會先查詢公開憑證並將其快取,直到憑證到期為止。您可以使用快取的公開憑證進行工作階段 Cookie 驗證,而不需要任何額外的網路要求。
如果 Cookie 無效,請確認 Cookie 已清除,並要求使用者登入 可以選取「重新建立」,再次生成新的提示您可以使用其他選項檢查工作階段撤銷。請注意,每次驗證工作階段 Cookie 時,這會額外增加網路要求。
出於安全考量,由於 Firebase 工作階段 Cookie 具有自訂有效期限 (可設定為最長 2 週),因此無法與其他 Firebase 服務搭配使用。所有使用伺服器端 Cookie 的應用程式,都應在驗證 Cookie 伺服器端後,強制執行權限檢查。
Node.js
getAuth()
.verifySessionCookie(sessionCookie, true)
.then((decodedClaims) => {
// Check custom claims to confirm user is an admin.
if (decodedClaims.admin === true) {
return serveContentForAdmin('/admin', req, res, decodedClaims);
}
res.status(401).send('UNAUTHORIZED REQUEST!');
})
.catch((error) => {
// Session cookie is unavailable or invalid. Force user to login.
res.redirect('/login');
});
Java
try {
final boolean checkRevoked = true;
FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(
sessionCookie, checkRevoked);
if (Boolean.TRUE.equals(decodedToken.getClaims().get("admin"))) {
return serveContentForAdmin(decodedToken);
}
return Response.status(Status.UNAUTHORIZED).entity("Insufficient permissions").build();
} catch (FirebaseAuthException e) {
// Session cookie is unavailable, invalid or revoked. Force user to login.
return Response.temporaryRedirect(URI.create("/login")).build();
}
Python
try:
decoded_claims = auth.verify_session_cookie(session_cookie, check_revoked=True)
# Check custom claims to confirm user is an admin.
if decoded_claims.get('admin') is True:
return serve_content_for_admin(decoded_claims)
return flask.abort(401, 'Insufficient permissions')
except auth.InvalidSessionCookieError:
# Session cookie is invalid, expired or revoked. Force user to login.
return flask.redirect('/login')
Go
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
// Session cookie is unavailable. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
decoded, err := client.VerifySessionCookieAndCheckRevoked(r.Context(), cookie.Value)
if err != nil {
// Session cookie is invalid. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Check custom claims to confirm user is an admin.
if decoded.Claims["admin"] != true {
http.Error(w, "Insufficient permissions", http.StatusUnauthorized)
return
}
serveContentForAdmin(w, r, decoded)
}
C#
try
{
var checkRevoked = true;
var decodedToken = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(
sessionCookie, checkRevoked);
object isAdmin;
if (decodedToken.Claims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
{
return ViewContentForAdmin(decodedToken);
}
return this.Unauthorized("Insufficient permissions");
}
catch (FirebaseAuthException)
{
// Session cookie is invalid or revoked. Force user to login.
return this.Redirect("/login");
}
登出
使用者登出用戶端時,透過 端點POST/GET 要求應會清除工作階段 Cookie。請注意,即使您清除 Cookie,它仍會保持有效狀態,直到自然到期為止。
Node.js
app.post('/sessionLogout', (req, res) => {
res.clearCookie('session');
res.redirect('/login');
});
Java
@POST
@Path("/sessionLogout")
public Response clearSessionCookie(@CookieParam("session") Cookie cookie) {
final int maxAge = 0;
NewCookie newCookie = new NewCookie(cookie, null, maxAge, true);
return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build();
}
Python
@app.route('/sessionLogout', methods=['POST'])
def session_logout():
response = flask.make_response(flask.redirect('/login'))
response.set_cookie('session', expires=0)
return response
Go
return func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
MaxAge: 0,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
C#
// POST: /sessionLogout
[HttpPost]
public ActionResult ClearSessionCookie()
{
this.Response.Cookies.Delete("session");
return this.Redirect("/login");
}
呼叫撤銷 API 會撤銷工作階段,並撤銷使用者的所有其他工作階段,強制使用者重新登入。對於敏感應用程式,建議縮短工作階段時間長度。
Node.js
app.post('/sessionLogout', (req, res) => {
const sessionCookie = req.cookies.session || '';
res.clearCookie('session');
getAuth()
.verifySessionCookie(sessionCookie)
.then((decodedClaims) => {
return getAuth().revokeRefreshTokens(decodedClaims.sub);
})
.then(() => {
res.redirect('/login');
})
.catch((error) => {
res.redirect('/login');
});
});
Java
@POST
@Path("/sessionLogout")
public Response clearSessionCookieAndRevoke(@CookieParam("session") Cookie cookie) {
String sessionCookie = cookie.getValue();
try {
FirebaseToken decodedToken = FirebaseAuth.getInstance().verifySessionCookie(sessionCookie);
FirebaseAuth.getInstance().revokeRefreshTokens(decodedToken.getUid());
final int maxAge = 0;
NewCookie newCookie = new NewCookie(cookie, null, maxAge, true);
return Response.temporaryRedirect(URI.create("/login")).cookie(newCookie).build();
} catch (FirebaseAuthException e) {
return Response.temporaryRedirect(URI.create("/login")).build();
}
}
Python
@app.route('/sessionLogout', methods=['POST'])
def session_logout():
session_cookie = flask.request.cookies.get('session')
try:
decoded_claims = auth.verify_session_cookie(session_cookie)
auth.revoke_refresh_tokens(decoded_claims['sub'])
response = flask.make_response(flask.redirect('/login'))
response.set_cookie('session', expires=0)
return response
except auth.InvalidSessionCookieError:
return flask.redirect('/login')
Go
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
// Session cookie is unavailable. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
decoded, err := client.VerifySessionCookie(r.Context(), cookie.Value)
if err != nil {
// Session cookie is invalid. Force user to login.
http.Redirect(w, r, "/login", http.StatusFound)
return
}
if err := client.RevokeRefreshTokens(r.Context(), decoded.UID); err != nil {
http.Error(w, "Failed to revoke refresh token", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
MaxAge: 0,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
C#
// POST: /sessionLogout
[HttpPost]
public async Task<ActionResult> ClearSessionCookieAndRevoke()
{
var sessionCookie = this.Request.Cookies["session"];
try
{
var decodedToken = await FirebaseAuth.DefaultInstance
.VerifySessionCookieAsync(sessionCookie);
await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(decodedToken.Uid);
this.Response.Cookies.Delete("session");
return this.Redirect("/login");
}
catch (FirebaseAuthException)
{
return this.Redirect("/login");
}
}
使用第三方 JWT 程式庫驗證工作階段 Cookie
如果後端使用 Firebase Admin SDK 不支援的語言, 仍可驗證工作階段 Cookie。首先,找出適用於您語言的第三方 JWT 程式庫。接著,請驗證工作階段 Cookie 的標頭、酬載和簽章。
驗證工作階段 Cookie 的標頭是否符合下列限制:
Firebase 工作階段 Cookie 標頭聲明 | ||
---|---|---|
alg |
演算法 | "RS256" |
kid |
金鑰 ID |
必須對應於下表列出的其中一個公開金鑰
https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys
|
驗證工作階段 Cookie 的酬載是否符合下列限制:
Firebase 工作階段 Cookie 酬載著作權聲明 | ||
---|---|---|
exp |
到期時間 | 必須是未來的時間。時間以秒為單位,從世界標準時間指定期間開始計算。到期時間會根據建立 Cookie 時提供的自訂時間長度設定。 |
iat |
核發時間 | 必須是過去的時間。時間的單位為從 UNIX 以來的時間 (以秒為單位) Epoch 時間。 |
aud |
目標對象 | 必須是 Firebase 專案 ID,也就是 Firebase 專案的專屬識別碼,可在該專案主控台的網址中找到。 |
iss |
核發單位 |
必須是 "https://session.firebase.google.com/<projectId>" ",
其中 <projectId> 與上述 aud 所使用的專案 ID 相同。
|
sub |
主旨 |
必須為非空白字串,且必須是使用者的 uid ,或
裝置。
|
auth_time
|
驗證時間 | 必須是過去的時間。使用者驗證的時間。這會與用於建立工作階段 Cookie 的 ID 權杖的 auth_time 相符。 |
最後,請確認工作階段 Cookie 已由符號對應的私密金鑰簽署。從以下位置取得公開金鑰:
https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys
並使用
用於驗證簽章的 JWT 程式庫。請使用
從該端點回應的 Cache-Control
標頭,判斷何時
重新整理公開金鑰。
如果上述所有驗證都成功,您可以使用工作階段 Cookie 的主體 (sub
) 做為對應使用者或裝置的使用者 ID。