Módulo 11 — Autenticação e Segurança

Você chegou a um módulo que toca em algo que todo desenvolvedor profissional precisa levar a sério: a segurança dos dados dos seus usuários. Nos módulos anteriores, você construiu um backend AWS completo para o aplicativo de delivery e integrou o Flutter a ele. O sistema funciona — mas qualquer pessoa com o URL do API Gateway poderia fazer pedidos em nome de qualquer usuário, consultar dados de qualquer conta, e até manipular registros do banco de dados. Isso é inaceitável em qualquer sistema real. Neste módulo, você vai corrigir isso implementando autenticação robusta com OAuth 2.0, armazenamento seguro de credenciais com flutter_secure_storage, autenticação biométrica com local_auth e proteção do backend com autorização baseada em tokens JWT.

A segurança não é uma funcionalidade que se adiciona no final do projeto — ela é uma propriedade que precisa ser construída desde o início. Mas como você está chegando a este tema no Módulo 11, a abordagem aqui será a de incorporar segurança retroativamente ao que já foi construído, o que é uma habilidade igualmente importante: a de analisar um sistema existente e identificar onde e como a segurança deve ser reforçada.

Estude cada seção com muita atenção. Os conceitos deste módulo são interligados de forma especialmente densa: você não vai conseguir implementar a autenticação biométrica sem entender os tokens de acesso, e não vai conseguir proteger o backend sem entender como os tokens são estruturados. A ordem das seções foi planejada para que cada conceito prepare o terreno para o seguinte.


Seção 1 — Autenticação, Autorização e Identidade

Antes de escrever uma linha de código relacionada a segurança, você precisa ter clareza absoluta sobre três termos que são frequentemente confundidos, mesmo por desenvolvedores experientes: autenticação, autorização e identidade. Confundi-los leva a decisões arquiteturais equivocadas que depois são difíceis de corrigir.

A autenticação é o processo de verificar que alguém é quem diz ser. No contexto do aplicativo de delivery, a autenticação responde à pergunta: “Quem está fazendo esta requisição?”. Quando um usuário fornece seu e-mail e senha e o sistema os valida contra os registros do banco de dados, esse processo é autenticação. O resultado da autenticação bem-sucedida é uma prova de identidade verificada — tipicamente um token que o sistema emissor assinou digitalmente e que pode ser apresentado em requisições subsequentes.

A autorização é o processo de determinar o que uma identidade autenticada pode fazer. A autorização responde à pergunta: “Esta requisição é permitida?”. No delivery, um usuário autenticado pode criar pedidos em seu próprio nome, mas não pode cancelar pedidos de outros usuários, acessar dados financeiros da plataforma ou modificar o cardápio. A autorização é sempre subsequente à autenticação: você só pode determinar o que alguém pode fazer depois de saber quem essa pessoa é.

A identidade é o conjunto de informações que descrevem um sujeito no sistema — nome, e-mail, identificador único, papéis (roles) e permissões. Em sistemas modernos, a identidade é frequentemente representada como um conjunto de claims — declarações sobre o sujeito — contidas em um token.

Esses três conceitos se relacionam de forma sequencial:

graph LR
    A[Usuário] -->|apresenta credenciais| B[Autenticação]
    B -->|gera| C[Token de Identidade]
    C -->|apresentado na requisição| D[Autorização]
    D -->|decide| E{Permitido?}
    E -->|Sim| F[Acessa recurso]
    E -->|Não| G[403 Forbidden]

Sessões vs Tokens: Por Que os Tokens Venceram

Durante décadas, o modelo dominante de autenticação em aplicações web foi baseado em sessões: após a autenticação, o servidor criava um objeto de sessão em memória ou banco de dados, associava a ele um identificador opaco (o session ID) e enviava esse identificador para o cliente como cookie. Em cada requisição subsequente, o cliente enviava o cookie, o servidor consultava o armazenamento de sessões para encontrar a sessão correspondente e verificava se ela ainda era válida.

Esse modelo funciona bem para aplicações web tradicionais com um único servidor, mas apresenta problemas significativos em arquiteturas distribuídas — exatamente o tipo de arquitetura que você está construindo com AWS Lambda. Quando um sistema tem múltiplas instâncias de serviço (no caso do Lambda, virtualmente infinitas), cada instância precisaria ter acesso ao mesmo armazenamento de sessões, o que cria um ponto central de falha e adiciona latência a cada requisição. Além disso, o modelo de sessões é fundamentalmente stateful — o servidor precisa manter estado sobre cada usuário conectado — o que contradiz o princípio stateless do REST que você estudou no Módulo 09.

Os tokens, especialmente os tokens JWT (JSON Web Tokens), resolvem todos esses problemas. Um JWT é um token autocontido: ele carrega dentro de si todas as informações necessárias para verificar sua validade e extrair os claims do usuário. O servidor não precisa consultar nenhum armazenamento externo — basta verificar a assinatura digital do token usando a chave pública do emissor. Isso torna a verificação de tokens completamente stateless e compatível com qualquer número de instâncias de serviço.


Seção 2 — JSON Web Tokens: Estrutura e Funcionamento

O JWT é o formato de token mais utilizado em sistemas de autenticação modernos, e você vai usá-lo ao longo de todo este módulo. Entender sua estrutura interna é importante não apenas para implementar a autenticação corretamente, mas também para depurar problemas — e eles inevitavelmente aparecem.

A Estrutura de Três Partes

Um JWT é uma string composta por três partes separadas por pontos: o cabeçalho (header), o payload e a assinatura. Cada parte é codificada em Base64URL — uma variante do Base64 que usa - e _ em vez de + e /, tornando a string segura para uso em URLs sem necessidade de codificação adicional.

O cabeçalho é um objeto JSON que especifica o tipo do token e o algoritmo de assinatura usado. O formato mais comum usa o algoritmo RS256 (RSA com SHA-256), que é assimétrico: o servidor de autorização assina o token com sua chave privada, e qualquer serviço que possua a chave pública correspondente pode verificar a assinatura sem precisar da chave privada.

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "delivery-key-2025-01"
}

O campo kid (Key ID) identifica qual chave pública deve ser usada para verificar a assinatura, o que permite a rotação de chaves sem invalidar todos os tokens existentes.

O payload contém os claims — as declarações sobre o sujeito e sobre o próprio token. Os claims padronizados pelo padrão RFC 7519 incluem:

Claim Nome completo Descrição
sub Subject Identificador único do usuário
iss Issuer Identificador do servidor que emitiu o token
aud Audience Identificador do serviço destinatário do token
exp Expiration Time Timestamp Unix da expiração do token
iat Issued At Timestamp Unix da emissão do token
jti JWT ID Identificador único do token

Além dos claims padronizados, o payload pode conter claims customizados. Para o delivery, o token de acesso de um usuário poderia ter a seguinte estrutura:

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "iss": "https://auth.delivery.example.com",
  "aud": "delivery-api",
  "exp": 1755820800,
  "iat": 1755817200,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "usuario@email.com",
  "nome": "Maria Silva",
  "roles": ["usuario"]
}

Verificação da Assinatura

A assinatura é calculada sobre a concatenação do cabeçalho codificado e do payload codificado, usando o algoritmo especificado no cabeçalho. No caso do RS256, a assinatura é calculada da seguinte forma:

\text{Assinatura} = \text{RSA-SHA256}\left(\text{RSASSA-PKCS1-v1\_5},\; k_{\text{priv}},\; \text{Base64URL}(\text{header}) \,\|\, \text{"."} \,\|\, \text{Base64URL}(\text{payload})\right)

onde k_{\text{priv}} é a chave privada do servidor de autorização e \| representa concatenação de strings. A verificação no lado do serviço receptor usa a chave pública correspondente k_{\text{pub}}:

\text{Válido} \iff \text{RSA-Verify}\left(k_{\text{pub}},\; \text{mensagem},\; \text{assinatura}\right) = \text{verdadeiro} \;\land\; \text{exp} > \text{now()}

A propriedade matemática central é que, dado apenas k_{\text{pub}}, é computacionalmente inviável forjar uma assinatura válida sem conhecer k_{\text{priv}}. Isso garante que um token só pode ter sido emitido por quem possui a chave privada — ou seja, pelo servidor de autorização legítimo.

A Importância do Campo exp

A expiração é um mecanismo de segurança fundamental. Um token JWT, uma vez emitido, não pode ser revogado pelo servidor de autorização (a menos que se mantenha uma lista negra, o que reintroduz o problema do estado). A única forma de limitar o dano causado por um token comprometido é fazê-lo expirar em um curto período de tempo. Tokens de acesso tipicamente têm vida útil de 15 a 60 minutos. Os tokens de atualização (refresh tokens) têm vida útil muito maior — horas, dias ou até semanas — mas são usados de forma diferente, como você verá na Seção 4.


Seção 3 — O Protocolo OAuth 2.0

OAuth 2.0 é o protocolo de autorização que você vai usar para implementar a autenticação dos usuários do delivery. É importante entender desde o início que o OAuth 2.0 é, tecnicamente, um protocolo de autorização — ele foi projetado para permitir que um aplicativo acesse recursos em nome de um usuário, sem que o aplicativo conheça as credenciais do usuário. A autenticação de usuários, como efeito colateral identificável do OAuth 2.0, se tornou o caso de uso dominante, e foi formalizada na especificação OpenID Connect (OIDC) construída sobre o OAuth 2.0.

O OAuth 2.0 define quatro papéis distintos:

O Resource Owner é o usuário — a pessoa que possui os dados e que pode conceder acesso a eles. No delivery, o resource owner é o usuário que quer fazer login no aplicativo.

O Client é o aplicativo que deseja acessar os dados em nome do resource owner. No delivery, o client é o aplicativo Flutter.

O Authorization Server é o servidor que autentica o resource owner e emite tokens de acesso após a autorização. Para o delivery, este poderia ser um servidor de autorização próprio, o AWS Cognito, ou qualquer provedor compatível com OAuth 2.0 como o Google ou o GitHub.

O Resource Server é o servidor que hospeda os recursos protegidos e aceita tokens de acesso para conceder acesso a eles. No delivery, o resource server é o API Gateway com as Lambda Functions.

Os Tipos de Fluxo (Grant Types)

O OAuth 2.0 define quatro tipos de fluxo (grant types), cada um adequado a diferentes cenários. O Authorization Code é o fluxo recomendado para aplicações que podem armazenar um segredo de forma segura — originalmente pensado para aplicações web server-side. O Implicit foi criado como simplificação do Authorization Code para aplicações de página única (SPAs), mas foi considerado inseguro e está sendo descontinuado. O Client Credentials é usado em comunicações máquina-a-máquina, sem envolvimento de um usuário humano. O Resource Owner Password Credentials (ROPC) requer que o usuário forneça suas credenciais diretamente ao client, o que vai contra o princípio de separação que o OAuth 2.0 visa estabelecer — também está sendo descontinuado.

Para aplicações móveis, nenhum desses quatro fluxos originais é completamente adequado da forma que foram originalmente especificados. O Authorization Code requer um segredo de cliente (client secret) que seria embutido no código do aplicativo — e código pode ser descompilado. O Implicit é inseguro por não verificar a integridade do token retornado. Foi por isso que a extensão PKCE foi desenvolvida.


Seção 4 — Authorization Code com PKCE para Aplicações Móveis

O PKCE — pronunciado “pixy”, do inglês Proof Key for Code Exchange — é uma extensão ao fluxo Authorization Code que elimina a necessidade de um client secret sem abrir mão da segurança. Especificado na RFC 7636, o PKCE foi originalmente criado para aplicações móveis, mas hoje é recomendado para qualquer tipo de client público — incluindo SPAs.

O Problema que o PKCE Resolve

O fluxo Authorization Code sem PKCE funciona assim: o client redireciona o usuário para o authorization server, onde ele faz login; o authorization server redireciona o usuário de volta para o client com um código de autorização (authorization code); o client troca esse código por um access token, apresentando também seu client secret para provar que é o client legítimo. O client secret impede que um atacante que intercepte o authorization code consiga trocá-lo por um token, pois o atacante não conhece o secret.

O problema em aplicações móveis é que não existe uma forma segura de armazenar um client secret. Um aplicativo instalado no dispositivo do usuário pode ser descompilado, e qualquer valor embutido no binário pode ser extraído. O PKCE resolve isso substituindo o client secret por um mecanismo criptográfico derivado de um valor aleatório gerado pelo client no momento de cada fluxo de autenticação.

O Mecanismo do PKCE Passo a Passo

Antes de iniciar o fluxo, o client gera um code_verifier: uma string aleatória criptograficamente segura com comprimento entre 43 e 128 caracteres, composta por caracteres do conjunto [A-Z a-z 0-9 - . _ ~]. Em seguida, o client calcula o code_challenge aplicando SHA-256 ao code_verifier e codificando o resultado em Base64URL:

\text{code\_challenge} = \text{Base64URL}\!\left(\text{SHA-256}\!\left(\text{ASCII}(\text{code\_verifier})\right)\right)

O code_verifier permanece guardado apenas na memória do client. O code_challenge é enviado ao authorization server como parte da requisição de autorização.

O authorization server armazena o code_challenge associado ao authorization code que emite. Quando o client apresenta o authorization code para trocá-lo por um access token, ele também envia o code_verifier. O authorization server calcula o hash do code_verifier recebido e verifica se o resultado corresponde ao code_challenge que havia armazenado. Se um atacante interceptou o authorization code, mas não tem acesso ao code_verifier (que nunca foi transmitido), não consegue obter o token.

sequenceDiagram
    participant A as Aplicativo Flutter
    participant B as Sistema Operacional
    participant C as Authorization Server
    participant D as API Gateway

    A->>A: gera code_verifier aleatório
    A->>A: code_challenge = Base64URL(SHA256(code_verifier))
    A->>B: abre URL de autorização + code_challenge
    B->>C: GET /authorize?response_type=code&code_challenge=...
    C->>B: tela de login do usuário
    B->>C: usuário faz login (email + senha)
    C->>B: redireciona com authorization_code
    B->>A: deep link com authorization_code
    A->>C: POST /token com authorization_code + code_verifier
    C->>C: verifica SHA256(code_verifier) == code_challenge
    C->>A: access_token + refresh_token + id_token
    A->>A: armazena tokens com flutter_secure_storage
    A->>D: GET /pedidos com Authorization: Bearer access_token
    D->>D: verifica assinatura do access_token
    D->>A: dados do pedido

O diagrama acima mostra o fluxo completo de Authorization Code com PKCE para o aplicativo de delivery. Observe que o code_verifier nunca sai do aplicativo Flutter — ele é gerado, guardado em memória e enviado diretamente ao authorization server na etapa de troca do código pelo token.


Seção 5 — Access Tokens e Refresh Tokens

Após um fluxo de autenticação bem-sucedido, o authorization server emite dois tipos de tokens que têm papéis muito distintos no ciclo de vida da sessão do usuário. Confundir suas funções ou armazená-los de forma inapropriada é um dos erros de segurança mais comuns em aplicações móveis.

O access token é a credencial que você apresenta ao resource server (o API Gateway) para provar que tem autorização para acessar um determinado recurso. Ele tem vida útil curta — tipicamente entre 15 e 60 minutos — e deve ser incluído no cabeçalho Authorization de cada requisição protegida, no formato Bearer {access_token}. O access token é frequentemente um JWT, o que permite que o resource server verifique sua validade localmente sem precisar consultar o authorization server.

O refresh token é uma credencial de longa duração — pode durar horas, dias ou semanas — que o aplicativo usa para obter um novo access token quando o atual expira, sem exigir que o usuário faça login novamente. Ao contrário do access token, o refresh token nunca é enviado ao resource server; ele vai apenas ao authorization server, no endpoint /token, para solicitar um novo access token. O refresh token é opacos — não é necessariamente um JWT e não contém informações legíveis pelo aplicativo.

Ciclo de Vida dos Tokens no Delivery

O ciclo de vida dos tokens no aplicativo de delivery segue uma lógica de renovação automática que é transparente para o usuário:

stateDiagram-v2
    [*] --> NaoAutenticado
    NaoAutenticado --> FluxoOAuth: usuário inicia login
    FluxoOAuth --> Autenticado: tokens recebidos
    Autenticado --> RequisicaoNormal: access_token válido
    RequisicaoNormal --> Autenticado: resposta 200
    Autenticado --> RenovandoToken: access_token expirado (401)
    RenovandoToken --> Autenticado: novo access_token obtido
    RenovandoToken --> NaoAutenticado: refresh_token expirado
    Autenticado --> NaoAutenticado: usuário faz logout

No estado Autenticado, o aplicativo inclui o access token em todas as requisições ao API Gateway. Quando o API Gateway retorna um código 401 (Unauthorized), o aplicativo sabe que o access token expirou. Em vez de redirecionar imediatamente o usuário para a tela de login, o aplicativo tenta renovar o access token usando o refresh token. Se a renovação for bem-sucedida, o aplicativo armazena o novo access token e refaz a requisição original. Se o refresh token também tiver expirado — o que acontece após um período prolongado sem uso — o aplicativo redireciona o usuário para a tela de login.

Por Que os Refresh Tokens Existem

Você pode se perguntar por que não simplesmente emitir access tokens de longa duração em vez de usar o mecanismo de refresh. A resposta está na granularidade do controle de revogação. Se um access token de longa duração for comprometido — por exemplo, se for capturado por um malware — ele continuará válido por um longo período. Com tokens de curta duração, a janela de oportunidade para o atacante é limitada.

Quando o servidor de autorização revoga um refresh token (por exemplo, porque o usuário relatou que o dispositivo foi roubado), ele invalida a capacidade do aplicativo de obter novos access tokens. Na próxima vez que o access token expirar — o que acontecerá em minutos —, a tentativa de renovação falhará e o usuário será desconectado. Esse mecanismo permite que a revogação de sessão seja efetiva em um tempo razoável sem sacrificar a performance do sistema com verificações de revogação em cada requisição.


Seção 6 — Implementando OAuth 2.0 com o Pacote oauth2_client

O pacote oauth2_client abstrai os detalhes do protocolo OAuth 2.0 e do PKCE, fornecendo uma API em Dart que torna a implementação do fluxo de autenticação acessível sem exigir que você implemente manualmente a geração do code_verifier, a construção das URLs de autorização e o gerenciamento do deep link de retorno.

Para adicionar o pacote ao projeto, inclua-o no pubspec.yaml:

dependencies:
  oauth2_client: ^4.2.3

Implementando o Serviço de Autenticação

Com o pacote configurado, você vai criar uma classe AutenticacaoServico na camada de infraestrutura do projeto. Essa classe encapsula toda a lógica OAuth 2.0 e expõe uma interface limpa para a camada de domínio:

import 'package:oauth2_client/oauth2_client.dart';
import 'package:oauth2_client/oauth2_helper.dart';
import 'package:oauth2_client/access_token_response.dart';
import '../../../dominio/autenticacao/i_autenticacao_repositorio.dart';
import '../../../dominio/autenticacao/modelo_usuario_autenticado.dart';
import 'dart:convert';

/// Cliente OAuth 2.0 configurado para o authorization server do delivery.
class DeliveryOAuth2Client extends OAuth2Client {
  DeliveryOAuth2Client()
      : super(
          authorizeUrl: 'https://auth.delivery.example.com/oauth2/authorize',
          tokenUrl: 'https://auth.delivery.example.com/oauth2/token',
          redirectUri: 'delivery.app://auth',
          customUriScheme: 'delivery.app',
        );
}

class AutenticacaoServico implements IAutenticacaoRepositorio {
  final DeliveryOAuth2Client _cliente;
  final OAuth2Helper _helper;

  AutenticacaoServico()
      : _cliente = DeliveryOAuth2Client(),
        _helper = OAuth2Helper(
          DeliveryOAuth2Client(),
          grantType: OAuth2Helper.authorizationCode,
          clientId: 'delivery-flutter-app',
          // Sem client secret — segurança garantida pelo PKCE
          scopes: ['openid', 'profile', 'email', 'delivery:pedidos'],
        );

  /// Inicia o fluxo Authorization Code com PKCE.
  /// Abre o navegador do sistema para que o usuário faça login.
  @override
  Future<ModeloUsuarioAutenticado> fazerLogin() async {
    final AccessTokenResponse resposta = await _helper.getToken();

    if (resposta.isValid()) {
      return _extrairUsuario(resposta);
    }

    throw Exception('Falha na autenticação: ${resposta.error}');
  }

  /// Usa o refresh token para obter um novo access token sem interação do usuário.
  @override
  Future<ModeloUsuarioAutenticado> renovarToken() async {
    final AccessTokenResponse resposta = await _helper.refreshToken();

    if (resposta.isValid()) {
      return _extrairUsuario(resposta);
    }

    throw Exception('Falha ao renovar token: ${resposta.error}');
  }

  /// Revoga os tokens e encerra a sessão.
  @override
  Future<void> fazerLogout() async {
    await _helper.disconnect();
  }

  /// Extrai os dados do usuário do ID token contido na resposta OAuth.
  ModeloUsuarioAutenticado _extrairUsuario(AccessTokenResponse resposta) {
    final String? idToken = resposta.getRespData('id_token') as String?;
    if (idToken == null) {
      throw Exception('ID token ausente na resposta do authorization server');
    }

    // O payload do JWT é a segunda parte, separada por pontos
    final List<String> partes = idToken.split('.');
    if (partes.length != 3) {
      throw const FormatException('ID token com formato inválido');
    }

    // Normaliza o Base64URL para Base64 padrão antes de decodificar
    String payload = partes[1];
    payload = payload.replaceAll('-', '+').replaceAll('_', '/');
    while (payload.length % 4 != 0) {
      payload += '=';
    }

    final Map<String, dynamic> claims =
        jsonDecode(utf8.decode(base64Decode(payload))) as Map<String, dynamic>;

    return ModeloUsuarioAutenticado(
      id: claims['sub'] as String,
      nome: claims['nome'] as String? ?? '',
      email: claims['email'] as String? ?? '',
      accessToken: resposta.accessToken ?? '',
      expiracaoAccessToken: DateTime.fromMillisecondsSinceEpoch(
        (claims['exp'] as int) * 1000,
      ),
    );
  }
}
import 'dart:convert';
import 'package:oauth2_client/oauth2_client.dart';
import 'package:oauth2_client/oauth2_helper.dart';
import 'package:oauth2_client/access_token_response.dart';
import '../../../dominio/autenticacao/i_autenticacao_repositorio.dart';
import '../../../dominio/autenticacao/modelo_usuario_autenticado.dart';

class _DeliveryOAuth2Client extends OAuth2Client {
  _DeliveryOAuth2Client()
      : super(
          authorizeUrl: 'https://auth.delivery.example.com/oauth2/authorize',
          tokenUrl: 'https://auth.delivery.example.com/oauth2/token',
          redirectUri: 'delivery.app://auth',
          customUriScheme: 'delivery.app',
        );
}

class AutenticacaoServico implements IAutenticacaoRepositorio {
  AutenticacaoServico()
      : _helper = OAuth2Helper(
          _DeliveryOAuth2Client(),
          grantType: OAuth2Helper.authorizationCode,
          clientId: 'delivery-flutter-app',
          scopes: ['openid', 'profile', 'email', 'delivery:pedidos'],
        );

  final OAuth2Helper _helper;

  @override
  Future<ModeloUsuarioAutenticado> fazerLogin() =>
      _helper.getToken().then(_validarEExtrair);

  @override
  Future<ModeloUsuarioAutenticado> renovarToken() =>
      _helper.refreshToken().then(_validarEExtrair);

  @override
  Future<void> fazerLogout() => _helper.disconnect();

  ModeloUsuarioAutenticado _validarEExtrair(AccessTokenResponse r) {
    if (!r.isValid()) throw Exception('OAuth erro: ${r.error}');
    return _extrairUsuario(r);
  }

  ModeloUsuarioAutenticado _extrairUsuario(AccessTokenResponse r) {
    final idToken = r.getRespData('id_token') as String? ??
        (throw Exception('id_token ausente'));
    final claims = _decodificarJwtPayload(idToken);
    return ModeloUsuarioAutenticado(
      id: claims['sub'] as String,
      nome: (claims['nome'] as String?) ?? '',
      email: (claims['email'] as String?) ?? '',
      accessToken: r.accessToken ?? '',
      expiracaoAccessToken:
          DateTime.fromMillisecondsSinceEpoch((claims['exp'] as int) * 1000),
    );
  }

  static Map<String, dynamic> _decodificarJwtPayload(String jwt) {
    var payload = jwt.split('.')[1].replaceAll('-', '+').replaceAll('_', '/');
    while (payload.length % 4 != 0) payload += '=';
    return jsonDecode(utf8.decode(base64Decode(payload))) as Map<String, dynamic>;
  }
}

Seção 7 — Armazenamento Seguro com flutter_secure_storage

Após a autenticação bem-sucedida, o aplicativo tem em mãos o access token e o refresh token. Onde guardá-los? A resposta incorreta, e surpreendentemente comum, é armazená-los em SharedPreferences. As preferências compartilhadas são armazenadas em um arquivo de texto simples no sistema de arquivos do dispositivo — qualquer processo com permissões suficientes pode lê-las. Em dispositivos com root ou jailbreak, isso é trivial. Em dispositivos sem root, aplicativos maliciosos ainda podem explorar vulnerabilidades do sistema operacional para acessar esses arquivos.

O flutter_secure_storage resolve esse problema usando os mecanismos de armazenamento seguro nativos de cada plataforma. No Android, a partir da versão 6.0 (API level 23), os dados são criptografados usando a classe EncryptedSharedPreferences da Android Jetpack Security, que por sua vez usa o Android Keystore para proteger as chaves de criptografia. As chaves do Keystore nunca saem do hardware de segurança do dispositivo — elas não podem ser extraídas, apenas usadas por operações criptográficas dentro do enclave seguro. No iOS, os dados são armazenados no Keychain, que oferece proteção equivalente usando o Secure Enclave do processador Apple.

Usando flutter_secure_storage no Projeto de Delivery

Adicione a dependência ao pubspec.yaml:

dependencies:
  flutter_secure_storage: ^10.0.0

No Android, é necessário definir a versão mínima do SDK como 23 no arquivo android/app/build.gradle:

defaultConfig {
    minSdkVersion 23
    ...
}

Com as configurações feitas, você vai criar um TokenRepositorio que centraliza todas as operações de leitura e escrita de tokens:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

/// Repositório responsável por persistir os tokens de autenticação
/// de forma segura no armazenamento nativo da plataforma.
class TokenRepositorio {
  // Chaves para identificar cada valor no armazenamento seguro
  static const String _chaveAccessToken = 'delivery_access_token';
  static const String _chaveRefreshToken = 'delivery_refresh_token';
  static const String _chaveExpiracaoToken = 'delivery_token_expiracao';
  static const String _chaveIdUsuario = 'delivery_usuario_id';

  final FlutterSecureStorage _armazenamento;

  TokenRepositorio({FlutterSecureStorage? armazenamento})
      : _armazenamento = armazenamento ??
            const FlutterSecureStorage(
              // Configuração Android: usa EncryptedSharedPreferences
              aOptions: AndroidOptions(
                encryptedSharedPreferences: true,
              ),
              // Configuração iOS: usa Keychain com acessibilidade após primeiro desbloqueio
              iOptions: IOSOptions(
                accessibility: KeychainAccessibility.first_unlock_this_device,
              ),
            );

  /// Persiste o access token e a data de expiração.
  Future<void> salvarAccessToken(String token, DateTime expiracao) async {
    await _armazenamento.write(key: _chaveAccessToken, value: token);
    await _armazenamento.write(
      key: _chaveExpiracaoToken,
      value: expiracao.toIso8601String(),
    );
  }

  /// Persiste o refresh token.
  Future<void> salvarRefreshToken(String token) async {
    await _armazenamento.write(key: _chaveRefreshToken, value: token);
  }

  /// Persiste o identificador do usuário autenticado.
  Future<void> salvarIdUsuario(String id) async {
    await _armazenamento.write(key: _chaveIdUsuario, value: id);
  }

  /// Lê o access token armazenado. Retorna null se não houver token.
  Future<String?> lerAccessToken() async {
    return _armazenamento.read(key: _chaveAccessToken);
  }

  /// Lê o refresh token armazenado. Retorna null se não houver token.
  Future<String?> lerRefreshToken() async {
    return _armazenamento.read(key: _chaveRefreshToken);
  }

  /// Verifica se o access token ainda está dentro do prazo de validade,
  /// com uma margem de 60 segundos para evitar expiração durante a requisição.
  Future<bool> accessTokenEstaValido() async {
    final String? expiracaoStr =
        await _armazenamento.read(key: _chaveExpiracaoToken);
    if (expiracaoStr == null) return false;

    final DateTime expiracao = DateTime.parse(expiracaoStr);
    final DateTime agora = DateTime.now();
    final Duration margemSeguranca = const Duration(seconds: 60);

    return expiracao.subtract(margemSeguranca).isAfter(agora);
  }

  /// Remove todos os tokens do armazenamento seguro (logout).
  Future<void> limparTodosOsTokens() async {
    await _armazenamento.deleteAll();
  }

  /// Verifica se existe uma sessão ativa (refresh token presente).
  Future<bool> temSessaoAtiva() async {
    final String? refreshToken = await lerRefreshToken();
    return refreshToken != null && refreshToken.isNotEmpty;
  }
}
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenRepositorio {
  static const _kAccessToken  = 'delivery_access_token';
  static const _kRefreshToken = 'delivery_refresh_token';
  static const _kExpiracao    = 'delivery_token_expiracao';
  static const _kIdUsuario    = 'delivery_usuario_id';

  static const _storage = FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
    iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
  );

  Future<void> salvarAccessToken(String token, DateTime expiracao) => Future.wait([
    _storage.write(key: _kAccessToken, value: token),
    _storage.write(key: _kExpiracao,   value: expiracao.toIso8601String()),
  ]);

  Future<void> salvarRefreshToken(String token) =>
      _storage.write(key: _kRefreshToken, value: token);

  Future<void> salvarIdUsuario(String id) =>
      _storage.write(key: _kIdUsuario, value: id);

  Future<String?> lerAccessToken()  => _storage.read(key: _kAccessToken);
  Future<String?> lerRefreshToken() => _storage.read(key: _kRefreshToken);

  Future<bool> accessTokenEstaValido() async {
    final exp = await _storage.read(key: _kExpiracao);
    if (exp == null) return false;
    return DateTime.parse(exp)
        .subtract(const Duration(seconds: 60))
        .isAfter(DateTime.now());
  }

  Future<void> limparTodosOsTokens() => _storage.deleteAll();

  Future<bool> temSessaoAtiva() async {
    final rt = await lerRefreshToken();
    return rt != null && rt.isNotEmpty;
  }
}

Opção de Acessibilidade do Keychain no iOS

A constante KeychainAccessibility.first_unlock_this_device merece uma explicação. Ela configura a item do Keychain para ser acessível após o primeiro desbloqueio do dispositivo após a inicialização, e somente no dispositivo atual (não é migrada em backups). Isso significa que o aplicativo pode acessar os tokens mesmo quando está rodando em background — importante para cenários de renovação automática de tokens — mas os tokens não estarão acessíveis se o dispositivo nunca tiver sido desbloqueado desde a inicialização. Existem outras opções de acessibilidade mais ou menos restritivas; first_unlock_this_device é um bom equilíbrio para a maioria dos aplicativos.


Seção 8 — Interceptores HTTP para Autorização Automática

Nos módulos anteriores, você fez requisições HTTP sem cabeçalho de autorização. Agora, cada requisição ao API Gateway precisa incluir o cabeçalho Authorization: Bearer {access_token}. Adicionar esse cabeçalho manualmente em cada chamada seria repetitivo e propenso a erros — esquecer o cabeçalho em uma única chamada resultaria em um 401 difícil de diagnosticar. A solução é implementar um interceptor: um componente que envolve o cliente HTTP e adiciona automaticamente o cabeçalho de autorização a cada requisição.

O pacote http padrão do Dart não oferece interceptores nativamente. Para implementar esse padrão, você vai criar um http.BaseClient customizado que delega as requisições a um cliente interno após adicionar o cabeçalho de autorização. Essa é a abordagem idiomática no ecossistema Dart:

import 'dart:async';
import 'package:http/http.dart' as http;
import '../autenticacao/token_repositorio.dart';

/// Cliente HTTP que adiciona automaticamente o token de autorização
/// e trata a renovação de tokens quando recebe um código 401.
class HttpClienteAutenticado extends http.BaseClient {
  final http.Client _clienteInterno;
  final TokenRepositorio _tokenRepositorio;
  final Future<void> Function() _renovarToken;

  /// [renovarToken] é uma callback chamada quando o token expira,
  /// responsável por executar o fluxo de refresh e atualizar o repositório.
  HttpClienteAutenticado({
    required TokenRepositorio tokenRepositorio,
    required Future<void> Function() renovarToken,
    http.Client? clienteInterno,
  })  : _clienteInterno = clienteInterno ?? http.Client(),
        _tokenRepositorio = tokenRepositorio,
        _renovarToken = renovarToken;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest requisicao) async {
    // 1. Verifica se o access token está válido; renova se necessário
    final bool tokenValido = await _tokenRepositorio.accessTokenEstaValido();
    if (!tokenValido) {
      await _renovarToken();
    }

    // 2. Lê o access token atual
    final String? accessToken = await _tokenRepositorio.lerAccessToken();
    if (accessToken == null) {
      throw StateError('Nenhum access token disponível após a renovação');
    }

    // 3. Adiciona o cabeçalho de autorização à requisição
    requisicao.headers['Authorization'] = 'Bearer $accessToken';

    // 4. Executa a requisição
    final http.StreamedResponse resposta = await _clienteInterno.send(requisicao);

    // 5. Se ainda assim receber 401, a sessão está encerrada
    if (resposta.statusCode == 401) {
      await _tokenRepositorio.limparTodosOsTokens();
      throw const SessaoExpiradaException('Sessão encerrada. Faça login novamente.');
    }

    return resposta;
  }

  @override
  void close() {
    _clienteInterno.close();
    super.close();
  }
}

/// Exceção lançada quando a sessão do usuário expira de forma irrecuperável.
class SessaoExpiradaException implements Exception {
  final String mensagem;
  const SessaoExpiradaException(this.mensagem);

  @override
  String toString() => mensagem;
}
import 'dart:async';
import 'package:http/http.dart' as http;
import '../autenticacao/token_repositorio.dart';

class SessaoExpiradaException implements Exception {
  const SessaoExpiradaException(this.mensagem);
  final String mensagem;
  @override String toString() => mensagem;
}

class HttpClienteAutenticado extends http.BaseClient {
  HttpClienteAutenticado({
    required TokenRepositorio tokenRepositorio,
    required Future<void> Function() renovarToken,
    http.Client? clienteInterno,
  })  : _repo = tokenRepositorio,
        _renovarToken = renovarToken,
        _inner = clienteInterno ?? http.Client();

  final http.Client _inner;
  final TokenRepositorio _repo;
  final Future<void> Function() _renovarToken;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest req) async {
    if (!await _repo.accessTokenEstaValido()) await _renovarToken();

    final token = await _repo.lerAccessToken()
        ?? (throw StateError('access token ausente'));

    req.headers['Authorization'] = 'Bearer $token';
    final resp = await _inner.send(req);

    if (resp.statusCode == 401) {
      await _repo.limparTodosOsTokens();
      throw const SessaoExpiradaException('Sessão encerrada');
    }
    return resp;
  }

  @override void close() { _inner.close(); super.close(); }
}

O Problema da Corrida de Renovação

Existe um problema sutil na implementação acima que precisa de atenção. Imagine que dez requisições são disparadas simultaneamente — por exemplo, ao carregar a tela inicial do delivery, que busca o cardápio, o perfil do usuário e os pedidos recentes em paralelo. Se o token estiver expirado no momento em que todas as dez requisições verificam sua validade, todas dez tentarão renovar o token ao mesmo tempo. Isso pode resultar em múltiplas chamadas ao endpoint /token do authorization server com o mesmo refresh token, o que muitos servidores de autorização interpretam como comportamento suspeito e revogam o refresh token.

A solução é serializar a renovação do token usando um Completer ou um Mutex que garanta que apenas uma renovação acontece por vez, e que as outras requisições aguardem o resultado da primeira:

import 'dart:async';

/// Gerenciador de renovação que previne chamadas concorrentes ao endpoint de token.
class GerenciadorRenovacaoToken {
  Future<void>? _renovacaoEmAndamento;

  /// Executa a renovação de token, garantindo que apenas uma execução
  /// ocorra por vez. Chamadas concorrentes aguardam a primeira completar.
  Future<void> renovar(Future<void> Function() funcaoRenovacao) {
    // Se já existe uma renovação em andamento, aguarda o resultado dela
    _renovacaoEmAndamento ??= funcaoRenovacao().whenComplete(() {
      _renovacaoEmAndamento = null;
    });
    return _renovacaoEmAndamento!;
  }
}
import 'dart:async';

class GerenciadorRenovacaoToken {
  Future<void>? _pendente;

  Future<void> renovar(Future<void> Function() fn) =>
      _pendente ??= fn().whenComplete(() => _pendente = null);
}

Seção 9 — Autenticação Biométrica com local_auth

A autenticação biométrica, por meio de impressão digital ou reconhecimento facial, é uma camada adicional de conveniência e segurança que os usuários esperam encontrar em aplicativos modernos. É importante entender o papel correto da biometria no contexto de segurança: ela não substitui a autenticação OAuth 2.0, mas complementa-a como uma forma de confirmação de presença do usuário no dispositivo.

O fluxo correto de integração é o seguinte: o usuário faz login com OAuth 2.0 uma única vez (ou na primeira vez que abre o aplicativo). A partir daí, os tokens são armazenados com segurança no flutter_secure_storage. Quando o usuário reabre o aplicativo após um período de inatividade ou após fechar o app, em vez de ser redirecionado para a tela de login completa (que exigiria interação com o authorization server), o aplicativo usa a biometria para confirmar que é o usuário legítimo do dispositivo — e então carrega os tokens do armazenamento seguro para restabelecer a sessão. Isso cria uma experiência de usuário muito mais fluida sem sacrificar a segurança.

Para adicionar a dependência:

dependencies:
  local_auth: ^3.0.0

No Android, adicione as permissões necessárias ao AndroidManifest.xml:

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>

Implementando o Serviço de Biometria

import 'package:local_auth/local_auth.dart';
import 'package:local_auth/error_codes.dart' as auth_error;
import 'package:flutter/services.dart';

/// Serviço que encapsula todas as operações de autenticação biométrica.
class BiometriaServico {
  final LocalAuthentication _auth;

  BiometriaServico({LocalAuthentication? auth})
      : _auth = auth ?? LocalAuthentication();

  /// Verifica se o dispositivo possui hardware biométrico disponível
  /// e se o usuário cadastrou pelo menos uma biometria.
  Future<bool> biometriaEstaDisponivel() async {
    // Verifica suporte a hardware biométrico
    final bool suportado = await _auth.isDeviceSupported();
    if (!suportado) return false;

    // Verifica se pode ser verificado (hardware ok e sem lock permanente)
    final bool podeVerificar = await _auth.canCheckBiometrics;
    if (!podeVerificar) return false;

    // Lista as biometrias disponíveis (impressão digital, face, etc.)
    final List<BiometricType> biometrias = await _auth.getAvailableBiometrics();
    return biometrias.isNotEmpty;
  }

  /// Solicita autenticação biométrica ao usuário.
  /// Retorna true se a autenticação for bem-sucedida.
  Future<bool> autenticar({
    required String motivoExibido,
  }) async {
    try {
      final bool autenticado = await _auth.authenticate(
        localizedReason: motivoExibido,
        options: const AuthenticationOptions(
          biometricOnly: false, // permite PIN como fallback
          stickyAuth: true,     // mantém o diálogo ativo se o app for para background
          sensitiveTransaction: true,
        ),
      );
      return autenticado;
    } on PlatformException catch (e) {
      // Erros específicos que podem ocorrer durante a autenticação
      switch (e.code) {
        case auth_error.notAvailable:
          // Hardware biométrico não disponível
          return false;
        case auth_error.notEnrolled:
          // Usuário não cadastrou nenhuma biometria
          return false;
        case auth_error.lockedOut:
          // Muitas tentativas falhas; dispositivo temporariamente bloqueado
          return false;
        case auth_error.permanentlyLockedOut:
          // Bloqueio permanente; usuário precisa usar PIN
          return false;
        default:
          return false;
      }
    }
  }
}
import 'package:local_auth/local_auth.dart';
import 'package:local_auth/error_codes.dart' as auth_error;
import 'package:flutter/services.dart';

class BiometriaServico {
  BiometriaServico({LocalAuthentication? auth})
      : _auth = auth ?? LocalAuthentication();

  final LocalAuthentication _auth;

  Future<bool> biometriaEstaDisponivel() async =>
      await _auth.isDeviceSupported() &&
      await _auth.canCheckBiometrics &&
      (await _auth.getAvailableBiometrics()).isNotEmpty;

  Future<bool> autenticar({required String motivoExibido}) async {
    try {
      return await _auth.authenticate(
        localizedReason: motivoExibido,
        options: const AuthenticationOptions(
          biometricOnly: false,
          stickyAuth: true,
          sensitiveTransaction: true,
        ),
      );
    } on PlatformException catch (e) {
      const codigosIgnorados = {
        auth_error.notAvailable,
        auth_error.notEnrolled,
        auth_error.lockedOut,
        auth_error.permanentlyLockedOut,
      };
      if (codigosIgnorados.contains(e.code)) return false;
      rethrow;
    }
  }
}

Integrando Biometria com o Fluxo de Autenticação

O componente que une o BiometriaServico com o TokenRepositorio é o SessaoNotifier — o Provider responsável por gerenciar o estado de autenticação em toda a aplicação. Quando o aplicativo é iniciado, o SessaoNotifier verifica se existe uma sessão ativa (refresh token presente no armazenamento seguro) e, em caso afirmativo, exige a confirmação biométrica antes de restaurar a sessão:

import 'package:flutter/foundation.dart';
import '../autenticacao/i_autenticacao_repositorio.dart';
import '../autenticacao/token_repositorio.dart';
import '../autenticacao/biometria_servico.dart';
import '../autenticacao/modelo_usuario_autenticado.dart';

enum EstadoSessao {
  verificando,
  autenticacaoBiometricaNecessaria,
  autenticado,
  naoAutenticado,
}

class SessaoNotifier extends ChangeNotifier {
  final IAutenticacaoRepositorio _autenticacaoRepositorio;
  final TokenRepositorio _tokenRepositorio;
  final BiometriaServico _biometriaServico;

  EstadoSessao _estado = EstadoSessao.verificando;
  ModeloUsuarioAutenticado? _usuario;
  String? _mensagemErro;

  EstadoSessao get estado => _estado;
  ModeloUsuarioAutenticado? get usuario => _usuario;
  String? get mensagemErro => _mensagemErro;

  SessaoNotifier({
    required IAutenticacaoRepositorio autenticacaoRepositorio,
    required TokenRepositorio tokenRepositorio,
    required BiometriaServico biometriaServico,
  })  : _autenticacaoRepositorio = autenticacaoRepositorio,
        _tokenRepositorio = tokenRepositorio,
        _biometriaServico = biometriaServico;

  /// Chamado ao iniciar o aplicativo para verificar o estado de autenticação.
  Future<void> verificarSessaoInicial() async {
    _estado = EstadoSessao.verificando;
    notifyListeners();

    final bool temSessao = await _tokenRepositorio.temSessaoAtiva();
    if (!temSessao) {
      _estado = EstadoSessao.naoAutenticado;
      notifyListeners();
      return;
    }

    // Existe uma sessão — verifica se deve usar biometria
    final bool biometriaDisponivel =
        await _biometriaServico.biometriaEstaDisponivel();

    if (biometriaDisponivel) {
      _estado = EstadoSessao.autenticacaoBiometricaNecessaria;
      notifyListeners();
    } else {
      // Sem biometria, restaura a sessão diretamente
      await _restaurarSessao();
    }
  }

  /// Executado quando o usuário confirma a biometria.
  Future<void> confirmarBiometria() async {
    final bool autenticado = await _biometriaServico.autenticar(
      motivoExibido: 'Confirme sua identidade para acessar o delivery',
    );

    if (autenticado) {
      await _restaurarSessao();
    } else {
      _mensagemErro = 'Autenticação biométrica falhou. Tente novamente.';
      notifyListeners();
    }
  }

  Future<void> _restaurarSessao() async {
    try {
      final ModeloUsuarioAutenticado usuario =
          await _autenticacaoRepositorio.renovarToken();
      _usuario = usuario;
      _estado = EstadoSessao.autenticado;
    } catch (e) {
      await _tokenRepositorio.limparTodosOsTokens();
      _estado = EstadoSessao.naoAutenticado;
    }
    notifyListeners();
  }

  Future<void> fazerLogin() async {
    try {
      final ModeloUsuarioAutenticado usuario =
          await _autenticacaoRepositorio.fazerLogin();
      _usuario = usuario;
      _estado = EstadoSessao.autenticado;
      notifyListeners();
    } catch (e) {
      _mensagemErro = 'Não foi possível fazer login. Tente novamente.';
      notifyListeners();
    }
  }

  Future<void> fazerLogout() async {
    await _autenticacaoRepositorio.fazerLogout();
    await _tokenRepositorio.limparTodosOsTokens();
    _usuario = null;
    _estado = EstadoSessao.naoAutenticado;
    notifyListeners();
  }
}
import 'package:flutter/foundation.dart';
import '../autenticacao/i_autenticacao_repositorio.dart';
import '../autenticacao/token_repositorio.dart';
import '../autenticacao/biometria_servico.dart';
import '../autenticacao/modelo_usuario_autenticado.dart';

enum EstadoSessao { verificando, biometriaNecessaria, autenticado, naoAutenticado }

class SessaoNotifier extends ChangeNotifier {
  SessaoNotifier({
    required IAutenticacaoRepositorio auth,
    required TokenRepositorio tokens,
    required BiometriaServico biometria,
  })  : _auth = auth, _tokens = tokens, _biometria = biometria;

  final IAutenticacaoRepositorio _auth;
  final TokenRepositorio _tokens;
  final BiometriaServico _biometria;

  var _estado = EstadoSessao.verificando;
  ModeloUsuarioAutenticado? _usuario;
  String? mensagemErro;

  EstadoSessao get estado => _estado;
  ModeloUsuarioAutenticado? get usuario => _usuario;

  void _mudar(EstadoSessao s) { _estado = s; notifyListeners(); }

  Future<void> verificarSessaoInicial() async {
    _mudar(EstadoSessao.verificando);
    if (!await _tokens.temSessaoAtiva()) { _mudar(EstadoSessao.naoAutenticado); return; }
    if (await _biometria.biometriaEstaDisponivel()) {
      _mudar(EstadoSessao.biometriaNecessaria);
    } else {
      await _restaurarSessao();
    }
  }

  Future<void> confirmarBiometria() async {
    if (!await _biometria.autenticar(motivoExibido: 'Confirme sua identidade')) {
      mensagemErro = 'Autenticação biométrica falhou';
      notifyListeners();
      return;
    }
    await _restaurarSessao();
  }

  Future<void> _restaurarSessao() async {
    try {
      _usuario = await _auth.renovarToken();
      _mudar(EstadoSessao.autenticado);
    } catch (_) {
      await _tokens.limparTodosOsTokens();
      _mudar(EstadoSessao.naoAutenticado);
    }
  }

  Future<void> fazerLogin() async {
    try {
      _usuario = await _auth.fazerLogin();
      _mudar(EstadoSessao.autenticado);
    } catch (e) {
      mensagemErro = 'Login falhou';
      notifyListeners();
    }
  }

  Future<void> fazerLogout() async {
    await Future.wait([_auth.fazerLogout(), _tokens.limparTodosOsTokens()]);
    _usuario = null;
    _mudar(EstadoSessao.naoAutenticado);
  }
}

Seção 10 — Protegendo o Backend: Autorização JWT no API Gateway

Com a autenticação implementada no lado do Flutter, o próximo passo é configurar o API Gateway para exigir um token JWT válido em todos os endpoints protegidos. No módulo anterior, você usou API Keys como mecanismo de autorização provisório. API Keys são adequadas para controlar o acesso entre sistemas, mas não identificam um usuário específico — qualquer pessoa com a chave pode fazer qualquer operação. Com JWT, cada token identifica o usuário e carrega suas permissões, permitindo uma autorização muito mais granular.

Lambda Authorizer: Verificação Personalizada de Tokens

O API Gateway suporta dois mecanismos de autorização baseados em JWT: o Cognito User Pool Authorizer, que funciona se você usa o AWS Cognito como authorization server; e o Lambda Authorizer, que é uma Lambda Function que você escreve e que recebe o token da requisição, o valida e retorna uma política IAM indicando se o acesso é permitido ou negado.

Para o projeto de delivery com um authorization server genérico, o Lambda Authorizer é a escolha adequada. Você vai criar uma Lambda Function delivery-authorizer que verifica a assinatura JWT e a validade do token:

import json
import os
import time
import urllib.request
import base64
import hashlib
import hmac

# Em produção, use uma biblioteca como python-jose ou PyJWT via Lambda Layer
# Este exemplo usa verificação simplificada para fins didáticos

ISSUER = os.environ.get('JWT_ISSUER', 'https://auth.delivery.example.com')
AUDIENCE = os.environ.get('JWT_AUDIENCE', 'delivery-api')


def decodificar_jwt_sem_verificar(token: str) -> tuple[dict, dict]:
    """Decodifica o header e payload de um JWT sem verificar a assinatura."""
    partes = token.split('.')
    if len(partes) != 3:
        raise ValueError("Formato JWT inválido")

    def decodificar_parte(parte: str) -> dict:
        padding = 4 - len(parte) % 4
        parte_padded = parte + '=' * (padding % 4)
        decoded = base64.urlsafe_b64decode(parte_padded)
        return json.loads(decoded)

    return decodificar_parte(partes[0]), decodificar_parte(partes[1])


def gerar_politica(efeito: str, arn_recurso: str, usuario_id: str) -> dict:
    """Gera a política IAM retornada ao API Gateway."""
    return {
        "principalId": usuario_id,
        "policyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "execute-api:Invoke",
                    "Effect": efeito,
                    "Resource": arn_recurso
                }
            ]
        },
        "context": {
            "usuarioId": usuario_id
        }
    }


def handler(event, context):
    token = event.get("authorizationToken", "")

    if not token.startswith("Bearer "):
        return gerar_politica("Deny", event["methodArn"], "desconhecido")

    jwt_token = token[7:]  # Remove o prefixo "Bearer "

    try:
        header, payload = decodificar_jwt_sem_verificar(jwt_token)

        # Verifica expiração
        exp = payload.get("exp", 0)
        if exp < int(time.time()):
            print(f"Token expirado: exp={exp}, now={int(time.time())}")
            return gerar_politica("Deny", event["methodArn"], "expirado")

        # Verifica issuer
        if payload.get("iss") != ISSUER:
            print(f"Issuer inválido: {payload.get('iss')}")
            return gerar_politica("Deny", event["methodArn"], "issuer_invalido")

        # Verifica audience
        aud = payload.get("aud", "")
        if aud != AUDIENCE and AUDIENCE not in (aud if isinstance(aud, list) else []):
            print(f"Audience inválido: {aud}")
            return gerar_politica("Deny", event["methodArn"], "audience_invalido")

        usuario_id = payload.get("sub", "")
        print(f"Token válido para usuário: {usuario_id}")
        return gerar_politica("Allow", event["methodArn"], usuario_id)

    except Exception as e:
        print(f"Erro ao processar token: {e}")
        return gerar_politica("Deny", event["methodArn"], "erro")

Após criar o Lambda Authorizer no console AWS, você o associa ao API Gateway: em cada método que precisa de proteção, configure o “Authorization” como o Lambda Authorizer criado. A partir daí, toda requisição ao endpoint sem um token JWT válido receberá automaticamente um código 403.

Propagando o ID do Usuário para as Lambda Functions

Uma vantagem do Lambda Authorizer é que ele pode passar dados extraídos do token para as Lambda Functions que processam a requisição, através do campo context da política retornada. No exemplo acima, o usuarioId é passado no contexto. Nas Lambda Functions de negócio, você acessa esse valor pelo objeto event:

def handler(event, context):
    # O usuarioId foi extraído do token pelo authorizer e passado aqui
    usuario_id = event.get("requestContext", {}).get("authorizer", {}).get("usuarioId")

    if not usuario_id:
        return {
            "statusCode": 401,
            "body": json.dumps({"erro": "usuário não identificado"})
        }

    # A partir daqui, você pode usar usuario_id para filtrar dados
    # ex: buscar apenas os pedidos do usuário autenticado
    ...

Isso elimina a necessidade de passar o ID do usuário no corpo das requisições — uma prática insegura que permitiria que um usuário mal-intencionado consultasse dados de outros usuários simplesmente alterando o ID no corpo da requisição.


Seção 11 — Ameaças Comuns em Aplicações Móveis e Como Mitigá-las

Construir uma autenticação segura é necessário, mas não suficiente. Uma aplicação móvel está exposta a um conjunto de ameaças que vão além do protocolo de autenticação. Nesta seção, você vai conhecer as principais categorias de vulnerabilidades que afetam aplicações móveis e as estratégias práticas para mitigá-las.

Armazenamento Inseguro de Dados Sensíveis

Você já aprendeu a armazenar tokens com flutter_secure_storage. O mesmo princípio se aplica a qualquer dado sensível do usuário — CPF, número de cartão, senhas, histórico financeiro. O erro mais comum é usar SharedPreferences para esses dados por conveniência. Dados sensíveis nunca devem residir em armazenamento sem criptografia.

Outra forma de armazenamento inseguro é o log de dados sensíveis. Cada print() que você escreve em modo de debug pode conter tokens, dados do usuário ou informações de autenticação. No aplicativo de delivery, configure o logging para nunca registrar o access token, o refresh token ou os dados pessoais do usuário. Em produção, desative completamente os prints de debug — o compilador do Flutter faz isso automaticamente em builds de release, mas é uma boa prática remover explicitamente qualquer impressão de dado sensível.

Comunicação Sem HTTPS

Toda comunicação entre o aplicativo Flutter e o API Gateway ocorre por HTTPS, o que protege os dados em trânsito usando TLS. No entanto, é importante verificar que a configuração do Android não permite tráfego HTTP não seguro. No arquivo android/app/src/main/AndroidManifest.xml, verifique se o atributo android:usesCleartextTraffic está ausente ou definido como false. Desde o Android 9 (API level 28), a configuração padrão já bloqueia tráfego HTTP, mas versões anteriores permitem por padrão — e o Flutter continua suportando dispositivos com Android 5.0 (API level 21) ou superior.

Exposição de Chaves de API no Código-Fonte

Qualquer valor hardcoded no código-fonte de um aplicativo pode ser extraído por alguém com acesso ao APK ou IPA. Isso inclui URLs de APIs internas, chaves de serviços de terceiros e identificadores de configuração. O client_id do OAuth 2.0, por exemplo, pode estar no código — ele é um identificador público. O que não pode estar é qualquer segredo, como o client_secret (que o PKCE elimina a necessidade de usar em aplicações móveis) ou chaves de APIs que devem ser mantidas confidenciais.

A estratégia correta para variáveis de configuração que variam por ambiente é usar --dart-define no momento da compilação, conforme você viu no Módulo 10. Para segredos que absolutamente precisam ser conhecidos pelo client (o que é raro e geralmente indica um problema de design), a alternativa é criar um endpoint backend que atua como proxy — o segredo fica no backend, e o client chama o backend para executar a operação que requer o segredo.

Validação de Certificado e Certificate Pinning

O HTTPS garante que a comunicação é criptografada e que o servidor tem um certificado emitido por uma autoridade certificadora confiável. No entanto, um atacante que consiga instalar um certificado de autoridade certificadora no dispositivo do usuário (o que é possível em ataques de man-in-the-middle direcionados) poderia interceptar a comunicação. O certificate pinning é uma técnica que associa o aplicativo a um certificado ou chave pública específica do servidor, de forma que o aplicativo rejeite qualquer outro certificado, mesmo que seja válido segundo as CAs do sistema operacional.

O certificate pinning adiciona segurança, mas também adiciona complexidade operacional: quando o certificado do servidor precisa ser renovado, o aplicativo também precisa ser atualizado e lançado na loja. Para o contexto do projeto de delivery, o HTTPS padrão é suficiente. Em ambientes de produção com requisitos de segurança elevados — como aplicações financeiras — o certificate pinning é uma medida adicional a considerar.

Proteção Contra Engenharia Reversa

Aplicativos Flutter compilados com flutter build apk --release ou flutter build appbundle --release são compilados ahead-of-time para código nativo (binário ARM), o que dificulta (mas não impossibilita) a engenharia reversa. Para o projeto de delivery no contexto desta disciplina, não são necessárias medidas adicionais de ofuscação. Em aplicativos comerciais com propriedade intelectual significativa, técnicas como ofuscação de código e proteção anti-tamper (como o ProGuard no Android) são utilizadas, mas estão fora do escopo desta disciplina.


Seção 12 — Integrando Autenticação no Roteamento com go_router

Com toda a infraestrutura de autenticação implementada, o último passo é integrá-la ao sistema de navegação do aplicativo. O go_router fornece o mecanismo de redirect que você estudou no Módulo 05, exatamente para esse propósito: redirecionar o usuário para a tela de login quando não está autenticado, e impedir que o usuário acesse a tela de login quando já está autenticado.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../autenticacao/sessao_notifier.dart';
import '../telas/tela_login.dart';
import '../telas/tela_biometria.dart';
import '../telas/tela_inicio.dart';
import '../telas/tela_cardapio.dart';
import '../telas/tela_meus_pedidos.dart';

GoRouter configurarRoteador(SessaoNotifier sessaoNotifier) {
  return GoRouter(
    initialLocation: '/',
    // O refreshListenable faz o roteador reavaliar os redirects
    // toda vez que o estado de sessão mudar
    refreshListenable: sessaoNotifier,
    redirect: (BuildContext context, GoRouterState state) {
      final EstadoSessao estadoSessao = sessaoNotifier.estado;
      final String localizacaoAtual = state.matchedLocation;

      // Enquanto verifica a sessão, não redireciona
      if (estadoSessao == EstadoSessao.verificando) return null;

      final bool naoAutenticado = estadoSessao == EstadoSessao.naoAutenticado;
      final bool biometriaNecessaria =
          estadoSessao == EstadoSessao.biometriaNecessaria;
      final bool naTelaLogin = localizacaoAtual == '/login';
      final bool naTelaBiometria = localizacaoAtual == '/biometria';

      // Usuário não autenticado tentando acessar rota protegida
      if (naoAutenticado && !naTelaLogin) return '/login';

      // Biometria necessária, mas não está na tela de biometria
      if (biometriaNecessaria && !naTelaBiometria) return '/biometria';

      // Usuário autenticado na tela de login ou biometria
      if (estadoSessao == EstadoSessao.autenticado &&
          (naTelaLogin || naTelaBiometria)) {
        return '/';
      }

      return null; // sem redirecionamento
    },
    routes: [
      GoRoute(path: '/login',     builder: (_, __) => const TelaLogin()),
      GoRoute(path: '/biometria', builder: (_, __) => const TelaBiometria()),
      ShellRoute(
        builder: (context, state, child) => TelaBase(child: child),
        routes: [
          GoRoute(path: '/',         builder: (_, __) => const TelaInicio()),
          GoRoute(path: '/cardapio', builder: (_, __) => const TelaCardapio()),
          GoRoute(path: '/pedidos',  builder: (_, __) => const TelaMeusPedidos()),
        ],
      ),
    ],
  );
}
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../autenticacao/sessao_notifier.dart';
import '../telas/tela_login.dart';
import '../telas/tela_biometria.dart';
import '../telas/tela_base.dart';
import '../telas/tela_inicio.dart';
import '../telas/tela_cardapio.dart';
import '../telas/tela_meus_pedidos.dart';

GoRouter configurarRoteador(SessaoNotifier notifier) => GoRouter(
      initialLocation: '/',
      refreshListenable: notifier,
      redirect: (_, state) {
        final loc = state.matchedLocation;
        return switch (notifier.estado) {
          EstadoSessao.verificando    => null,
          EstadoSessao.naoAutenticado => loc == '/login' ? null : '/login',
          EstadoSessao.biometriaNecessaria =>
            loc == '/biometria' ? null : '/biometria',
          EstadoSessao.autenticado =>
            (loc == '/login' || loc == '/biometria') ? '/' : null,
        };
      },
      routes: [
        GoRoute(path: '/login',     builder: (_, __) => const TelaLogin()),
        GoRoute(path: '/biometria', builder: (_, __) => const TelaBiometria()),
        ShellRoute(
          builder: (_, __, child) => TelaBase(child: child),
          routes: [
            GoRoute(path: '/',         builder: (_, __) => const TelaInicio()),
            GoRoute(path: '/cardapio', builder: (_, __) => const TelaCardapio()),
            GoRoute(path: '/pedidos',  builder: (_, __) => const TelaMeusPedidos()),
          ],
        ),
      ],
    );

O ponto mais elegante desta implementação é o refreshListenable: sessaoNotifier. Por padrão, o go_router avalia os redirects apenas quando o usuário navega entre rotas. O refreshListenable instrui o roteador a reavaliar os redirects também quando o SessaoNotifier chama notifyListeners() — ou seja, toda vez que o estado de autenticação muda. Com isso, quando o token expira e o SessaoNotifier atualiza seu estado para naoAutenticado, o roteador automaticamente redireciona o usuário para a tela de login, independentemente de qual tela ele estivesse visualizando.


Seção 13 — Configuração do Authorization Server para Desenvolvimento

Para testar o fluxo OAuth 2.0 localmente durante o desenvolvimento, você precisa de um authorization server que suporte PKCE e possa ser configurado com os parâmetros do delivery. Existem duas abordagens práticas: usar o AWS Cognito, que integra naturalmente com o backend AWS que você criou no Módulo 10, ou usar um servidor de autorização de desenvolvimento como o oauth2-proxy ou o Keycloak rodando localmente.

AWS Cognito como Authorization Server

O AWS Cognito é o serviço de autenticação e autorização gerenciado da AWS. Ele oferece um User Pool — um diretório de usuários com funcionalidades de login, registro, recuperação de senha e verificação de e-mail — e um App Client, que representa sua aplicação Flutter no Cognito.

Para configurar o Cognito para o delivery, você precisa criar um User Pool no console AWS, configurar o domínio de autenticação (que gera URLs no formato https://{seu-dominio}.auth.{regiao}.amazoncognito.com), e registrar um App Client com o tipo de fluxo Authorization code grant, o redirect URI delivery.app://auth e os escopos openid, profile e email.

Os endpoints do Cognito seguem a especificação OpenID Connect Discovery, o que significa que o oauth2_client pode ser configurado para trabalhar com ele da mesma forma que com qualquer outro authorization server compatível com OAuth 2.0. Basta substituir as URLs de autorização e token nas configurações do cliente:

class DeliveryOAuth2Client extends OAuth2Client {
  DeliveryOAuth2Client({
    required String cognitoDominio,
    required String regiao,
  }) : super(
    authorizeUrl: 'https://$cognitoDominio.auth.$regiao.amazoncognito.com/oauth2/authorize',
    tokenUrl: 'https://$cognitoDominio.auth.$regiao.amazoncognito.com/oauth2/token',
    redirectUri: 'delivery.app://auth',
    customUriScheme: 'delivery.app',
  );
}

O Cognito emite tokens JWT com a estrutura padrão que você estudou neste módulo, e fornece um endpoint JWKS (JSON Web Key Set) público que pode ser usado para verificar as assinaturas dos tokens. O Lambda Authorizer que você criou na Seção 10 pode ser adaptado para buscar as chaves públicas do Cognito nesse endpoint e usá-las para verificação criptográfica completa — em vez da verificação simplificada que implementamos anteriormente.

Testando a Autenticação com Postman

Para testar o fluxo OAuth 2.0 com PKCE diretamente no Postman (sem o aplicativo Flutter), configure uma requisição no Postman com autorização do tipo OAuth 2.0. O Postman suporta nativamente o fluxo Authorization Code com PKCE, gerando automaticamente o code_verifier e o code_challenge e executando o fluxo completo em um navegador embutido. Isso permite verificar que o authorization server está configurado corretamente e que os tokens emitidos têm a estrutura esperada antes de integrar com o Flutter.


Seção 14 — Revogação de Sessão e Logout Seguro

O logout parece uma operação simples — basta apagar os tokens do dispositivo. No entanto, para que o logout seja verdadeiramente seguro, ele precisa também invalidar os tokens no lado do servidor. Sem isso, um atacante que tiver capturado o refresh token continuará podendo usá-lo para obter novos access tokens mesmo depois que o usuário se desconectou.

A operação completa de logout no aplicativo de delivery envolve três etapas que devem ser executadas em sequência. Primeiro, o aplicativo envia o refresh token ao endpoint de revogação do authorization server, invalidando-o definitivamente no lado do servidor. Segundo, o aplicativo apaga todos os tokens do flutter_secure_storage. Terceiro, o SessaoNotifier atualiza seu estado para naoAutenticado e notifica o go_router, que redireciona o usuário para a tela de login.

O endpoint de revogação de tokens é padronizado na RFC 7009 e geralmente está disponível como POST /oauth2/revoke no authorization server. O pacote oauth2_client expõe isso através do método disconnect() do OAuth2Helper, que cuida da chamada ao endpoint de revogação automaticamente. É por isso que o fazerLogout() do AutenticacaoServico chama _helper.disconnect() em vez de simplesmente limpar os tokens localmente.

Vale destacar que a revogação do refresh token não invalida imediatamente os access tokens que já foram emitidos. Um access token que expira em 30 minutos continuará sendo aceito pelo API Gateway durante esse tempo, mesmo após a revogação do refresh token. Isso é uma consequência da natureza stateless dos JWTs: o Lambda Authorizer verifica a assinatura e a expiração, mas não consulta o authorization server para saber se o token foi revogado. A mitigação é manter o tempo de expiração do access token curto, de forma que a janela de exposição após um logout seja mínima.

Para aplicativos com requisitos de segurança elevados, uma lista de tokens revogados (token denylist ou token blacklist) pode ser mantida no Redis ou no DynamoDB, consultada pelo Lambda Authorizer em cada requisição. Essa abordagem aumenta a segurança à custa de uma chamada de rede adicional em cada verificação — um custo que é aceitável em sistemas financeiros ou de saúde, mas que pode ser desproporcional para a maioria dos aplicativos comerciais.

Logout em Múltiplos Dispositivos

Considere o cenário em que um usuário está logado no delivery em dois dispositivos: o celular pessoal e o tablet. Se o usuário faz logout em um dispositivo, ele continua logado no outro. Isso pode ser o comportamento desejado — é o que acontece na maioria das aplicações como Google e Spotify. Mas em alguns cenários de segurança, como quando o usuário relata que um dispositivo foi perdido ou roubado, é necessário desconectar todos os dispositivos simultaneamente.

Esse nível de controle requer que o backend mantenha um registro de todas as sessões ativas por usuário — cada refresh token associado ao dispositivo que o solicitou. Quando um logout global é solicitado, o backend invalida todos os refresh tokens do usuário de uma vez. Essa funcionalidade é específica do sistema e não é padronizada pelo OAuth 2.0, mas pode ser implementada como um endpoint adicional no backend do delivery: POST /usuarios/logout-global, que recebe o token de acesso do usuário, extrai o sub (ID do usuário) e remove todas as sessões associadas a esse ID.


Seção 15 — Testando a Autenticação: Estratégias e Boas Práticas

Testar código de autenticação tem particularidades que o distinguem de outros tipos de teste. O fluxo OAuth 2.0 envolve redirecionamentos, navegadores externos e tokens com tempo de vida definido — nada disso é fácil de simular em um ambiente de teste automatizado. Ainda assim, existem estratégias que permitem testar boa parte da lógica de autenticação de forma eficaz.

Testando o SessaoNotifier com Mocks

A maior parte da lógica de autenticação do Flutter reside no SessaoNotifier, e ele pode ser testado sem precisar de uma conexão real com o authorization server. A chave é injetar mocks das dependências — IAutenticacaoRepositorio, TokenRepositorio e BiometriaServico — que simulam comportamentos específicos.

Um teste para o fluxo de inicialização quando existe uma sessão ativa e biometria disponível poderia verificar que o SessaoNotifier transita corretamente para o estado biometriaNecessaria. Um teste para o fluxo de confirmação de biometria bem-sucedida poderia verificar que o estado transita para autenticado e que o usuário correto é armazenado. Um teste para o fluxo de renovação de token falha — quando o refresh token também está expirado — poderia verificar que os tokens são apagados e o estado transita para naoAutenticado.

Essa cobertura de testes para o SessaoNotifier é especialmente valiosa porque os erros de lógica em autenticação frequentemente resultam em bugs difíceis de reproduzir — como o usuário sendo desconectado aleatoriamente ou permanecer logado após solicitar o logout.

Testando o HttpClienteAutenticado

O HttpClienteAutenticado também pode ser testado com mocks. Um teste importante é o que verifica o comportamento quando o token está expirado: o cliente deve renovar o token antes de fazer a requisição, e não deve fazer a requisição com um token expirado. Outro teste importante é o que verifica o comportamento quando a API retorna 401 mesmo após a renovação — o cliente deve lançar SessaoExpiradaException e apagar os tokens.

Esses testes são escritos contra a interface http.BaseClient, mockando o cliente HTTP interno com uma implementação que retorna respostas pré-definidas:

import 'package:test/test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';

void main() {
  group('HttpClienteAutenticado', () {
    test('adiciona cabeçalho Authorization quando token está válido', () async {
      http.Request? requisicaoCapturada;
      final clienteMock = MockClient((req) async {
        requisicaoCapturada = req as http.Request;
        return http.Response('{}', 200);
      });

      // ... configurar o HttpClienteAutenticado com mocks de TokenRepositorio
      // que retornam um token válido e verificar que o cabeçalho foi adicionado
    });
  });
}

Integrando com um Authorization Server de Teste

Para testes de integração que precisam de um fluxo OAuth 2.0 real, uma alternativa é configurar o AWS Cognito em modo de teste — criando um user pool específico para testes com usuários fictícios — ou usar uma biblioteca como o MockOAuth2Server disponível para linguagens como Kotlin e Java, que pode simular um authorization server em memória.

Para o contexto desta disciplina, o mais prático é realizar testes manuais do fluxo completo usando o emulador Android ou um dispositivo físico, validando cada etapa com observação do estado do SessaoNotifier através dos logs do Flutter e verificando os tokens no CloudWatch Logs do Lambda Authorizer.


Seção 16 — Glossário de Segurança e Referência Rápida

Este módulo introduziu um grande volume de terminologia específica de segurança. Este glossário serve como referência rápida para os termos mais usados, que você vai reencontrar nos próximos módulos e na sua carreira profissional.

A tabela a seguir consolida os principais termos e conceitos estudados neste módulo:

Termo Definição
Autenticação Processo de verificar que um sujeito é quem afirma ser
Autorização Processo de determinar o que um sujeito autenticado pode fazer
OAuth 2.0 Protocolo de autorização que permite que um app acesse recursos em nome de um usuário
OpenID Connect (OIDC) Camada de identidade construída sobre OAuth 2.0, que padroniza a autenticação de usuários
JWT JSON Web Token — formato padrão para tokens autocontidos com assinatura digital
Access Token Token de curta duração usado para acessar recursos protegidos
Refresh Token Token de longa duração usado para obter novos access tokens sem novo login
ID Token Token JWT que contém claims de identidade do usuário; emitido pelo OIDC
Authorization Code Código temporário emitido pelo authorization server, trocado por tokens
PKCE Proof Key for Code Exchange — extensão do fluxo Authorization Code para clients públicos
code_verifier String aleatória secreta gerada pelo client para o fluxo PKCE
code_challenge Hash SHA-256 do code_verifier, enviado ao authorization server
Scope Permissão específica solicitada pelo client (ex: email, delivery:pedidos)
Claim Declaração sobre o sujeito contida em um token (ex: sub, email, exp)
Issuer (iss) Identificador do servidor que emitiu o token
Audience (aud) Identificador do serviço destinatário do token
Subject (sub) Identificador único do usuário no authorization server
Bearer Token Esquema de autenticação HTTP onde o portador do token tem acesso
JWKS JSON Web Key Set — conjunto de chaves públicas para verificação de JWTs
Lambda Authorizer Função Lambda que verifica tokens e retorna políticas IAM para o API Gateway
flutter_secure_storage Pacote Flutter para armazenamento seguro usando Keychain (iOS) e EncryptedSharedPreferences (Android)
local_auth Pacote Flutter para autenticação biométrica com impressão digital e face
Cold Start (biometria) Primeiro acesso após reinicialização, quando o Keychain ainda não está acessível
Revogação Invalidação de um refresh token no authorization server, impedindo sua reutilização

Fluxo de Decisão: Qual Token Usar em Cada Situação

Para consolidar o entendimento sobre o uso correto de cada tipo de token, o diagrama a seguir resume as decisões que o aplicativo toma em cada cenário:

graph TD
    A[Aplicativo precisa fazer\nrequisição ao API Gateway] --> B{Tem access token?}
    B -->|Não| C{Tem refresh token?}
    B -->|Sim| D{Access token ainda válido?}
    D -->|Sim| E[Envia requisição com\nAccess Token]
    D -->|Não| C
    C -->|Não| F[Redireciona para login\nFluxo OAuth 2.0 completo]
    C -->|Sim| G[Chama endpoint /token\ncom Refresh Token]
    G --> H{Refresh token aceito?}
    H -->|Sim| I[Armazena novo\nAccess Token]
    H -->|Não| F
    I --> E
    E --> J{API retorna 401?}
    J -->|Não| K[Processa resposta]
    J -->|Sim| L[Limpa tokens\nRedireciona para login]


Resumo do Módulo

Este módulo foi denso em conceitos interdependentes, e vale a pena consolidar o caminho que você percorreu. Você começou compreendendo a distinção entre autenticação, autorização e identidade — conceitos que formam a base de qualquer raciocínio sobre segurança. Em seguida, mergulhou na estrutura dos tokens JWT, compreendendo como a assinatura criptográfica garante que um token só pode ter sido emitido pelo servidor de autorização legítimo, e como a expiração limita a janela de exposição em caso de comprometimento.

O protocolo OAuth 2.0 e sua extensão PKCE formaram o núcleo do módulo. Você entendeu por que o PKCE existe, qual problema ele resolve e como o mecanismo de code_verifier e code_challenge garante a segurança do fluxo mesmo em ambientes onde um segredo de client não pode ser protegido — exatamente o caso de aplicações móveis. O pacote oauth2_client abstraiu a complexidade protocolar, mas o entendimento subjacente que você construiu permite depurar problemas e fazer escolhas arquiteturais informadas.

O flutter_secure_storage garantiu que os tokens, uma vez obtidos, sejam armazenados de forma inacessível a outras aplicações e processos no dispositivo. O interceptor HTTP com renovação automática de tokens tornou essa segurança transparente para o restante da aplicação. O local_auth adicionou a camada de confirmação biométrica, melhorando simultaneamente a segurança e a experiência do usuário.

No backend, o Lambda Authorizer transformou o API Gateway de um endpoint aberto em uma barreira que aceita apenas requisições com tokens válidos, e que propaga o ID do usuário autenticado para as Lambda Functions de negócio — eliminando a possibilidade de um usuário acessar dados de outro simplesmente alterando um parâmetro de requisição.

Por fim, a integração com o go_router tornou a navegação consciente do estado de autenticação, garantindo que o usuário seja automaticamente direcionado para a tela correta conforme sua sessão evolui.

No Módulo 12, você vai expandir as capacidades de comunicação do backend adicionando notificações push com Firebase Cloud Messaging. O sistema que você configurou neste módulo — com o Lambda Authorizer identificando cada usuário pelo ID do token — é exatamente o que permitirá, no Módulo 12, associar o token FCM de cada dispositivo ao usuário correspondente, possibilitando o envio de notificações direcionadas quando o status de um pedido é atualizado.