Gerenciar cookies de sessão

O Firebase Authentication fornece gerenciamento de cookies de sessão no servidor para sites tradicionais que dependem de cookies de sessão. Esta solução tem várias vantagens sobre os tokens de código de curta duração do cliente, que podem exigir um mecanismo de redirecionamento em cada utilização para poder atualizar o cookie de sessão ao expirar:

  • Segurança aprimorada por meio de tokens de sessão baseados no JWT que somente podem ser gerados por meio de contas de serviço autorizadas.
  • Cookies de sessão sem estado com todos os benefícios do uso de JWTs para autenticação. O cookie de sessão tem as mesmas declarações (incluindo as personalizadas) que o token de código. Assim, as mesmas verificações de permissão se aplicam aos cookies de sessão.
  • Capacidade de criar cookies de sessão com tempos de expiração personalizados que variam de 5 minutos a 2 semanas.
  • Flexibilidade para aplicar políticas de cookies baseadas nos requisitos do aplicativo: domínio, caminho, proteção, httpOnly, etc.
  • Possibilidade de revogar cookies de sessão quando houver suspeita de roubo de token por meio da API de revogação do token de atualização existente.
  • Capacidade de detectar a revogação da sessão nas principais mudanças da conta.

Fazer login

Supondo que um aplicativo esteja usando cookies httpOnly no servidor, faça login de um usuário na página de login usando os SDKs do cliente. Um token de código do Firebase é gerado e enviado via HTTP POST para um ponto de extremidade de login de sessão onde, por meio do SDK Admin, um cookie de sessão é gerado. Se tudo der certo, o estado deverá ser removido do armazenamento do cliente.

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

Para gerar um cookie de sessão em troca do token de código fornecido, é necessário um ponto de extremidade HTTP. Envie o token para o ponto de extremidade, definindo um tempo de duração de sessão personalizado usando o SDK Admin do Firebase. Medidas apropriadas devem ser tomadas para evitar ataques de falsificação de solicitações entre sites (CSRF, na sigla em inglês).

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

Para aplicativos confidenciais, o parâmetro auth_time deve ser verificado antes de gerar o cookie de sessão, o que minimiza a janela de ataque caso um token de código seja roubado:

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

Após o login, todas as seções protegidas por acesso ao site devem analisar o cookie de sessão e verificá-lo antes de veicular conteúdo restrito com base em alguma regra de segurança.

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

Verifique os cookies da sessão usando a API verifySessionCookie do SDK Admin. Esta é uma operação de baixa sobrecarga. Os certificados públicos são inicialmente consultados e armazenados em cache até expirarem. A verificação de cookie de sessão pode ser feita com os certificados públicos em cache, sem solicitações de rede adicionais.

Se o cookie for inválido, verifique se ele foi limpo e peça para que o usuário faça login novamente. Uma opção adicional está disponível para verificar a revogação da sessão. Este procedimento adiciona uma outra solicitação de rede toda vez que um cookie de sessão é verificado.

Por motivos de segurança, os cookies de sessão do Firebase não podem ser usados ​​com outros serviços da plataforma. Isso acontece devido ao período de validade personalizado deles, que pode ser definido para a duração máxima de duas semanas. Todos os aplicativos que usam cookies do servidor precisam aplicar as análises de permissões após a verificação desses cookies.

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

Sair

Quando um usuário se desconecta do cliente, use um ponto de extremidade para processar essa operação. Uma solicitação POST/GET deve resultar na limpeza do cookie de sessão. Embora o cookie seja apagado, ele permanecerá ativo até sua expiração natural.

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

Chamar a API de revogação interrompe esta e todas as outras sessões do usuário, forçando um novo login. Para aplicativos confidenciais, recomendamos uma duração da sessão mais curta.

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

Verificar cookies de sessão usando uma biblioteca de JWT de terceiros

Caso seu back-end esteja em uma linguagem incompatível com o SDK Admin do Firebase, ainda é possível verificar os cookies de sessão. Primeiro, encontre uma biblioteca de JWT de terceiros para sua linguagem. Em seguida, verifique o cabeçalho, o payload e a assinatura do cookie de sessão.

Verifique se o cabeçalho do cookie de sessão está em conformidade com as seguintes restrições:

Declarações de cabeçalho de cookies de sessão do Firebase
alg Algoritmo "RS256"
kid Código da chave Precisa corresponder a uma das chaves públicas listadas em https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys.

Verifique se o payload do cookie de sessão está em conformidade com as seguintes restrições:

Declarações do payload de cookies de sessão do Firebase
exp Expiração Precisa estar no futuro. O tempo é medido em segundos desde a época do Unix. A expiração é definida com base na duração personalizada fornecida após a criação do cookie.
iat Emitido em Precisa estar no passado. O tempo é medido em segundos desde a época do Unix.
aud Público Precisa ser seu código do projeto do Firebase, o identificador exclusivo do seu projeto do Firebase que pode ser encontrado no URL do console desse projeto.
iss Emissor Precisa ser "https://session.firebase.google.com/<projectId>", sendo que <projectId> é o mesmo código do projeto usado para o atributo aud acima.
sub Assunto Precisa ser uma string que não esteja em branco e precisa ser o uid do usuário ou dispositivo.
auth_time Tempo de autenticação Precisa estar no passado. A hora em que o usuário realizou a autenticação. O valor corresponde ao atributo auth_time do token de código usado para criar o cookie de sessão.

Por fim, verifique se o cookie de sessão foi assinado pela chave privada correspondente à declaração kid do token. Veja a chave pública em https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys e use uma biblioteca de JWT para verificar a assinatura. Use o valor de idade máxima no cabeçalho Cache-Control da resposta desse ponto de extremidade para determinar quando atualizar as chaves públicas.

Se todas as verificações acima forem realizadas com sucesso, você poderá usar o assunto (sub) do cookie de sessão como o uid do usuário ou dispositivo correspondente.

Enviar comentários sobre…

Precisa de ajuda? Acesse nossa página de suporte.