セッション Cookie を管理する

Firebase Auth は、セッション Cookie に依存する従来型のウェブサイト向けに、サーバー側でのセッション Cookie の管理機能を提供します。たとえば、クライアント側の短期の ID トークンでは、セッション Cookie を期限切れの時点で更新するリダイレクト メカニズムが毎回必要になる場合がありますが、それと比べると、このソリューションには以下のようないくつかの利点があります。

  • 承認済みのサービス アカウントを使用する場合のみ生成可能な JWT ベースのセッション トークンによるセキュリティの強化。
  • 認証に JWT を使用するさまざまな利点が得られるステートレス セッション Cookie。このセッション Cookie の要件は(カスタムの要件を含め)ID トークンと同じであるため、セッション Cookie に同じ権限チェックを適用できます。
  • 5 分から 2 週間までの範囲でカスタムの有効期限を使用してセッション Cookie を作成する機能。
  • ドメイン、パス、セキュア、httpOnly など、アプリケーションの要件に基づいて Cookie ポリシーを適用できる柔軟性。
  • トークンの盗用が疑われる場合に、既存の更新トークン取り消し 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');
});

指定された ID トークンと引き換えにセッション Cookie を生成するには、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')

セッション Cookie の確認には、Admin SDK の verifySessionCookie API を使用します。これは低オーバーヘッドのオペレーションです。公開証明書は、最初に照会され、有効期限が切れるまでキャッシュされます。セッション Cookie の確認は、キャッシュされている公開証明書を使用して行うことができ、追加のネットワーク リクエストは不要です。

Cookie が無効である場合は、必ず Cookie をクリアして、再度ログインするようユーザーに依頼します。セッションの取り消しをチェックする追加のオプションも使用できます。セッション Cookie を確認するたびに、追加のネットワーク リクエストが発生することに注意してください。

セキュリティ上の理由により、Firebase のセッション Cookie は、最長期間を 2 週間に設定できるカスタムの有効期限があるため、他の Firebase サービスでは使用できません。サーバー側 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 がクリアされた場合であっても、本来の期限切れまで 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 プロジェクトの一意の識別子)であることが必要です。これは、プロジェクトのコンソールの URL で確認できます。
iss 発行元 "https://session.firebase.google.com/<projectId>" であることが必要です。<projectId> は、上記の aud で使用された同じプロジェクト ID です。
sub 件名 空ではない文字列、またはユーザーまたは端末の uid であることが必要です。
auth_time 認証時間 過去の時点であることが必要です。ユーザーが認証を行った時間です。これは、セッション Cookie の作成に使用される ID トークンの auth_time に一致します。

最後に、トークンの kid 要件に対応する秘密鍵によってセッション Cookie が署名されたことを確認します。https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。該当エンドポイントからのレスポンスの Cache-Control ヘッダーに含まれる max-age の値を使用して、公開鍵を更新する時期を判別します。

上記のすべての確認が完了したら、対応するユーザーまたは端末の uid として、セッション Cookie の件名(sub)を使用できます。

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。