管理会话 Cookie

Firebase 身份验证为依赖会话 Cookie 的传统网站提供了服务器端会话 Cookie 管理功能。客户端 ID 令牌的有效时间短,可能会在每次到期时要求进行重定向以更新会话 Cookie,与此相比,Firebase 身份验证提供的解决方案具有多重优势:

  • 通过基于 JWT 的会话令牌(只能使用授权的服务帐号生成)增强了安全性。
  • 无状态会话 Cookie,可实现使用 JWT 进行身份验证的所有好处。这种会话 Cookie 与 ID 令牌具备同样的声明(包括自定义声明),因而支持对会话 Cookie 执行同样的权限检查。
  • 能够创建具备自定义有效期(从 5 分钟到 2 周)的会话 Cookie。
  • 能够根据应用要求灵活地执行 Cookie 政策:网域、路径、安全、httpOnly 等等。
  • 当怀疑出现令牌被盗的情况时,能够使用现有的刷新令牌撤消 API 来撤消会话 Cookie。
  • 能够在出现重大帐号变化时检测会话撤消情况。

登录

假设有一个使用 httpOnly 服务器端 Cookie 的应用,该应用使用客户端 SDK 使一个用户在登录页面上登录了该应用。这时,系统会生成一个 Firebase ID 令牌,然后该 ID 令牌将通过 HTTP POST 发送至一个会话登录端点。在该端点上,系统将使用 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 令牌,您需要一个 HTTP 端点。将令牌发送到该端点,并使用 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.
  admin.auth().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 auth.AuthError:
        return flask.abort(401, 'Failed to create a session cookie')

对于敏感型应用,您应在发出会话 Cookie 之前检查 auth_time,以便万一出现 ID 令牌被盗时使攻击时间尽可能较短。

Node.js

admin.auth().verifyIdToken(idToken).then((decodedIdTokens) => {
  // 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 admin.auth().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 ValueError:
    return flask.abort(401, 'Invalid ID token')
except auth.AuthError:
    return flask.abort(401, 'Failed to create a session 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.
  admin.auth().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')
    # 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 ValueError:
        # Session cookie is unavailable or invalid. Force user to login.
        return flask.redirect('/login')
    except auth.AuthError:
        # Session revoked. Force user to login.
        return flask.redirect('/login')

使用 Admin SDK verifySessionCookie API 验证会话 Cookie。这是一项低开销的操作。系统会首先查询公共证书,并将其缓存直至过期。会话 Cookie 验证流程可以使用缓存的公共证书完成,而无需任何其他网络请求。

如果 Cookie 无效,请确保将其清除,并要求用户重新登录。另外,还有一个选项可用于检查会话撤消。请注意,这会在每次验证会话 Cookie 时增加一个额外的网络请求。

出于安全原因,Firebase 会话 Cookie 无法与其他 Firebase 服务一起使用,因为这种 Cookie 的自定义有效期限最长可以设置为 2 周。所有使用服务器端 Cookie 的应用都需要在服务器端验证这些 Cookie 后执行权限检查。

Node.js

admin.auth().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)
    else:
        return flask.abort(401, 'Insufficient permissions')
except ValueError:
    # Session cookie is unavailable or invalid. Force user to login.
    return flask.redirect('/login')
except auth.AuthError:
    # Session revoked. Force user to login.
    return flask.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

调用撤消 API 可撤消当前会话及该用户的所有其他会话,并强制重新登录。对于敏感型应用,建议您设置较短的会话持续时间。

Node.js

app.post('/sessionLogout', (req, res) => {
  const sessionCookie = req.cookies.session || '';
  res.clearCookie('session');
  admin.auth().verifySessionCookie(sessionCookie).then((decodedClaims) => {
    return admin.auth().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 ValueError:
        return flask.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 到期时间 必须是将来的时间。时间从 UNIX 计时原点开始计算,以秒为单位。到期时间根据创建 Cookie 时提供的自定义持续时间进行设置。
iat 颁发时间 必须是过去时间。时间从 UNIX 计时原点开始计算,以秒 为单位。
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) 作为相应用户或设备的 uid。

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面