Mengelola Cookie Sesi

Firebase Auth menyediakan pengelolaan cookie sesi sisi server untuk situs tradisional yang bergantung pada cookie sesi. Solusi ini memiliki beberapa keunggulan dibandingkan dengan token ID sisi klien yang berumur pendek, yang mungkin memerlukan mekanisme pengalihan setiap kali akan mengupdate cookie sesi pada akhir masa berlakunya:

  • Peningkatan keamanan melalui token sesi berbasis JWT yang hanya dapat dibuat menggunakan akun layanan resmi.
  • Cookie sesi stateless yang dilengkapi dengan semua manfaat penggunaan JWT untuk autentikasi. Cookie sesi tersebut memiliki klaim yang sama (termasuk klaim khusus) dengan token ID, sehingga pemeriksaan izin yang sama dapat diberlakukan pada cookie sesi.
  • Kemampuan untuk membuat cookie sesi dengan waktu habis masa berlaku kustom yang berkisar dari 5 menit hingga 2 minggu.
  • Fleksibilitas untuk menerapkan kebijakan cookie berdasarkan kebutuhan aplikasi: domain, lokasi, keamanan, httpOnly, dll.
  • Kemampuan untuk mencabut cookie sesi ketika diduga ada pencurian token menggunakan API pencabutan token refresh yang ada.
  • Kemampuan untuk mendeteksi pencabutan sesi pada perubahan akun utama.

Login

Buat agar pengguna login menggunakan SDK klien, dengan asumsi bahwa sebuah aplikasi menggunakan cookie sisi server httpOnly. Setelah token ID Firebase dibuat dan dikirim melalui HTTP POST ke lokasi endpoint login sesi menggunakan Admin SDK, cookie sesi akan dibuat. Setelah berhasil dibuat, statusnya harus dihapus dari penyimpanan sisi klien.

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');
});

Endpoint HTTP diperlukan untuk membuat cookie sesi setelah token ID dibuat. Kirimkan token ke endpoint, yang akan menyetel waktu durasi sesi kustom menggunakan Firebase Admin SDK. Langkah-langkah yang tepat harus diambil untuk mencegah serangan pemalsuan permintaan lintas situs (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')

Untuk aplikasi yang sensitif, auth_time harus diperiksa sebelum cookie sesi dikeluarkan. Hal tersebut dapat meminimalkan potensi serangan seandainya token ID dicuri:

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')

Setelah login, semua bagian situs dengan akses terlindung harus memeriksa cookie sesi dan memverifikasinya sebelum menyajikan konten yang dibatasi berdasarkan beberapa aturan keamanan.

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')

Lakukan verifikasi pada cookie sesi menggunakan verifySessionCookie API Admin SDK. Ini merupakan operasi overhead rendah. Kueri sertifikat publik akan dibuat dan disimpan dalam cache sampai masa berlakunya habis. Verifikasi cookie sesi dapat dilakukan dengan sertifikat publik yang disimpan dalam cache tanpa permintaan jaringan tambahan.

Jika cookie tidak valid, pastikan cookie tersebut dihapus, dan minta pengguna untuk login kembali. Tersedia pilihan tambahan yang dapat diperiksa untuk pencabutan sesi. Perlu diperhatikan bahwa tindakan ini akan menambahkan permintaan jaringan tambahan setiap kali cookie sesi diverifikasi.

Untuk alasan keamanan, cookie sesi Firebase tidak dapat digunakan dengan layanan Firebase lainnya karena periode validitas khusus cookie tersebut, yang dapat disetel ke durasi maksimum yakni 2 minggu. Semua aplikasi yang menggunakan cookie sisi server diharapkan menerapkan pemeriksaan izin setelah memverifikasi cookie ini pada sisi server.

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')

Logout

Ketika pengguna logout dari sisi klien, tangani hal tersebut di sisi server melalui endpoint. Permintaan POST/GET akan menyebabkan cookie sesi dihapus. Perlu diperhatikan bahwa meskipun dihapus, cookie sesi akan tetap aktif hingga masa berlakunya habis.

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

Jika API pencabutan dipanggil, sesi akan dicabut, begitu juga dengan semua sesi pengguna lainnya. Dengan begitu, proses login baru harus dilakukan. Untuk aplikasi yang sensitif, sebaiknya gunakan durasi sesi yang lebih pendek.

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')

Memverifikasi cookie sesi menggunakan library JWT pihak ketiga

Jika backend Anda menggunakan bahasa yang tidak didukung Firebase Admin SDK, Anda masih dapat memverifikasi cookie sesi. Pertama-tama, temukan library JWT pihak ketiga untuk bahasa Anda. Kemudian, verifikasi header, payload, dan tanda tangan cookie sesi.

Verifikasikan bahwa header cookie sesi sesuai dengan batasan berikut:

Klaim Header Cookie Sesi Firebase
alg Algoritme "RS256"
kid ID Kunci Harus sesuai dengan salah satu kunci publik yang tercantum di https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys

Verifikasikan bahwa payload cookie sesi sesuai dengan batasan berikut:

Klaim Payload Cookie Sesi Firebase
exp Waktu habis masa berlaku Harus di masa depan. Waktu diukur dalam hitungan detik sejak periode UNIX. Akhir masa berlaku disetel berdasarkan durasi khusus yang diberikan saat cookie dibuat.
iat Waktu penerbitan Harus di masa lalu. Waktu diukur dalam hitungan detik sejak periode UNIX.
aud Audience Harus berupa project ID Firebase Anda, pengenal unik untuk project Firebase Anda, yang dapat ditemukan di URL pada konsol project tersebut.
iss Penerbit Harus berupa "https://session.firebase.google.com/<projectId>", dengan <projectId> merupakan project ID yang sama untuk aud di atas.
sub Subjek Harus berupa string yang tidak kosong dan harus berupa uid pengguna atau perangkat.
auth_time Waktu autentikasi Harus di masa lalu. Waktu ketika pengguna diautentikasi. Harus cocok dengan auth_time token ID yang digunakan untuk membuat cookie sesi.

Terakhir, pastikan cookie sesi telah ditandatangani melalui kunci pribadi yang sesuai dengan klaim kid token. Dapatkan kunci publik dari https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys dan gunakan library JWT untuk memverifikasi tanda tangan. Gunakan nilai usia maksimum di header Cache-Control pada respons dari endpoint tersebut untuk mengetahui kapan harus memperbarui kunci publik.

Jika semua verifikasi di atas berhasil, Anda dapat menggunakan subjek (sub) dari cookie sesi sebagai uid pengguna atau perangkat yang sesuai.

Kirim masukan tentang...

Butuh bantuan? Kunjungi halaman dukungan kami.