Módulo 11 — Exercícios: Autenticação e Segurança

Chegamos ao módulo que conecta tudo que você aprendeu sobre Flutter com o problema mais sensível de qualquer aplicação em produção: saber quem é o usuário, garantir que ele é quem diz ser, e proteger cada camada da pilha de acesso não autorizado. Neste módulo você implementou OAuth 2.0 com PKCE, estruturou o armazenamento seguro de tokens com flutter_secure_storage, construiu um cliente HTTP que renova credenciais de forma transparente, habilitou autenticação biométrica com local_auth, e escreveu um Lambda Authorizer em Python que valida JWTs antes de qualquer requisição chegar à lógica de negócio. Os três exercícios a seguir percorrem esse caminho de forma progressiva: o primeiro estabelece a fundação do armazenamento e da biometria; o segundo integra o interceptor HTTP com a máquina de estados de sessão; o terceiro fecha o ciclo com o Lambda Authorizer e o roteamento protegido com go_router. Leia cada enunciado com cuidado antes de escrever qualquer linha de código, pois as decisões de design que você tomar aqui têm consequências diretas na segurança do aplicativo.


Exercício 1

TokenRepositorio e BiometriaServico: a fundação do armazenamento seguro

Antes de autenticar, é preciso saber guardar com segurança

Toda a cadeia de autenticação do seu aplicativo de delivery repousa sobre dois pilares: um lugar confiável para guardar os tokens que provam a identidade do usuário, e um mecanismo para confirmar, na reabertura do aplicativo, que a pessoa que está segurando o dispositivo é a mesma que realizou o login. Sem o primeiro pilar, os tokens ficam expostos a qualquer processo com acesso ao armazenamento do dispositivo. Sem o segundo, um dispositivo desbloqueado ou compartilhado oferece acesso irrestrito à conta do usuário sem nenhuma fricção adicional. Este exercício trata exatamente disso: você vai implementar o TokenRepositorio e o BiometriaServico, que juntos formam a infraestrutura de segurança de mais baixo nível do aplicativo, e vai integrá-los em uma tela de splash que usa os dois para tomar a decisão de para onde navegar.

O contexto importa aqui. Você já sabe que SharedPreferences é inadequado para tokens porque os arquivos por ele gerados não são protegidos pelo sistema operacional contra outros processos — em um dispositivo com root, qualquer aplicação pode ler esses arquivos sem restrição. O flutter_secure_storage resolve esse problema de formas distintas em cada plataforma: no Android, ele usa o EncryptedSharedPreferences combinado com o Android Keystore, que mantém a chave de criptografia em hardware isolado; no iOS, ele usa o Keychain protegido pelo Secure Enclave. A consequência prática é que, mesmo que um atacante obtenha o arquivo de armazenamento, não consegue ler os dados sem a chave, que nunca sai do hardware.

O primeiro componente que você deve implementar é o TokenRepositorio. Essa classe é responsável por toda a persistência e recuperação dos tokens de autenticação, e deve ser construída de forma a nunca vazar informações sensíveis. Para instanciar o FlutterSecureStorage, use as opções específicas de plataforma: no Android, configure encryptedSharedPreferences: true dentro de AndroidOptions; no iOS, configure accessibility: KeychainAccessibility.first_unlock_this_device dentro de IOSOptions. Essa opção específica do iOS tem uma implicação importante que você deve compreender: ela instrui o Keychain a tornar o dado disponível apenas após o primeiro desbloqueio do dispositivo após uma reinicialização, e marca o dado como não migrável para outros dispositivos via backup do iCloud. Isso significa que se um usuário restaurar um novo iPhone a partir de um backup, os tokens não serão transferidos — o que é o comportamento correto para credenciais de segurança.

A instância de FlutterSecureStorage deve ser recebida pelo construtor com um valor padrão, permitindo que em testes unitários você substitua o armazenamento real por um mock sem alterar nenhum código de produção. Utilize as seguintes chaves de string para persistência: 'delivery_access_token', 'delivery_refresh_token', 'delivery_token_expiracao' e 'delivery_usuario_id'.

Os métodos obrigatórios do TokenRepositorio são cinco. O método salvarSessao deve receber um access token, um refresh token, a data e hora de expiração do access token e o ID do usuário, e persistir os quatro valores em paralelo usando Future.wait. Essa decisão não é arbitrária: quatro escritas sequenciais com await encadeados levariam quatro vezes mais tempo que escritas paralelas, e o momento de salvar a sessão — imediatamente após um login — é exatamente quando o usuário aguarda a tela principal aparecer. Os métodos lerAccessToken e lerRefreshToken fazem leituras simples e diretas. O método accessTokenEstaValido lê a data de expiração armazenada, retorna false imediatamente se ela for nula, e em seguida verifica se o token ainda terá validade daqui a pelo menos 60 segundos — essa margem existe para evitar que um token expire durante o trânsito da requisição até o servidor. Use DateTime.parse(exp).subtract(const Duration(seconds: 60)).isAfter(DateTime.now()) para essa verificação. O método temSessaoAtiva retorna true apenas se o refresh token lido for não-nulo e não-vazio, pois é o refresh token que define se há uma sessão recuperável — o access token pode estar expirado, mas enquanto houver um refresh token válido, a sessão pode ser renovada. Por fim, o método limparTodosOsTokens deve chamar deleteAll(), que remove todos os valores persistidos pelo flutter_secure_storage para esta aplicação.

O segundo componente é o BiometriaServico, que encapsula o LocalAuthentication do pacote local_auth. Da mesma forma que o TokenRepositorio, ele deve receber uma instância de LocalAuthentication pelo construtor com um valor padrão, garantindo a injetabilidade em testes. O método biometriaEstaDisponivel deve retornar true somente se três condições forem verdadeiras simultaneamente: isDeviceSupported() retornar true, canCheckBiometrics retornar true, e getAvailableBiometrics() retornar uma lista não vazia. As três verificações devem ser feitas antes de tentar a autenticação para evitar exceções desnecessárias.

O método autenticar deve receber um parâmetro nomeado motivoExibido e passá-lo como localizedReason para _auth.authenticate(), configurado com AuthenticationOptions(biometricOnly: false, stickyAuth: true, sensitiveTransaction: true). A opção biometricOnly: false permite que o usuário use o PIN do dispositivo como fallback quando a biometria falha após algumas tentativas — o que é especialmente importante para usuários idosos ou com dificuldades com leitores biométricos. A opção stickyAuth: true mantém o diálogo de autenticação aberto quando o aplicativo vai para segundo plano momentaneamente. A opção sensitiveTransaction: true instrui o sistema operacional a exibir uma mensagem mais explícita sobre o que está sendo autorizado.

A parte mais importante do método autenticar é o tratamento de exceções. O local_auth lança PlatformException com códigos específicos para diferentes situações: notAvailable quando o hardware não está disponível, notEnrolled quando nenhuma biometria está registrada, lockedOut quando o leitor está temporariamente bloqueado após muitas tentativas incorretas, e permanentlyLockedOut quando o bloqueio requer intervenção do usuário para ser desfeito. Para esses quatro códigos, o método deve capturar a exceção e retornar false. Para qualquer outro código de PlatformException — por exemplo, um erro inesperado de hardware — o método deve relançar a exceção. Importar package:local_auth/error_codes.dart como auth_error permite usar as constantes auth_error.notAvailable, auth_error.notEnrolled, auth_error.lockedOut e auth_error.permanentlyLockedOut para fazer a comparação sem usar strings literais.

O terceiro componente é a TelaSplash, um StatefulWidget que recebe o TokenRepositorio e o BiometriaServico pelo construtor. Em initState, ela deve disparar um método assíncrono _verificarEstadoInicial() que executa a seguinte lógica: primeiro, verifica se há sessão ativa com temSessaoAtiva(); se não houver, navega para /login; se houver, verifica se a biometria está disponível; se estiver, solicita a autenticação com o motivo 'Confirme sua identidade para continuar'; se bem-sucedida, navega para /home; se falhar, exibe um SnackBar com a mensagem 'Autenticação biométrica falhou' e permanece na tela de splash aguardando nova tentativa; se a biometria não estiver disponível, navega diretamente para /home. Durante toda a verificação, a tela deve exibir um CircularProgressIndicator centralizado. Para navegar, use Navigator.pushReplacementNamed. Em cada ponto do código que sucede um await, verifique mounted antes de chamar setState ou qualquer método de navegação, pois o widget pode ter sido removido da árvore enquanto a operação assíncrona estava em andamento.

Implemente os três componentes em um único arquivo g_ex1.dart, que deve conter também uma função main com um MaterialApp simples para demonstrar o funcionamento. Não use SharedPreferences em nenhum contexto relacionado a tokens. Não imprima tokens com print em nenhuma circunstância, pois logs de aplicação podem ser capturados por ferramentas de depuração em dispositivos não confiáveis.

Antes de implementar, reflita sobre três questões que vão além da sintaxe. Por que Future.wait é preferível a quatro chamadas await sequenciais no método salvarSessao? O que acontece com os dados do Keychain iOS quando o dispositivo é restaurado a partir de um backup, e por que a configuração first_unlock_this_device evita que os tokens migrem para outro dispositivo? Por que relançar uma PlatformException com código desconhecido no método autenticar é a decisão correta, em vez de simplesmente retornar false e tratar o problema silenciosamente?

O método authenticate do local_auth pode lançar PlatformException com códigos que não estão entre os quatro tratados explicitamente — por exemplo, se o hardware biométrico apresentar uma falha inesperada associada a um código proprietário do fabricante do dispositivo. Retornar false silenciosamente nesses casos faria com que o aplicativo se comportasse como se a biometria simplesmente não funcionasse, sem nenhuma mensagem de erro e sem nenhum registro que permitisse diagnosticar o problema. O padrão correto é tratar apenas os códigos que você conhece e compreende, e relançar todos os demais para que a camada superior possa decidir o que fazer — seja exibir uma mensagem de erro diferenciada, seja registrar o evento em um sistema de monitoramento.

O que deve ser entregue: um arquivo chamado g_ex1.dart, onde g é o nome do seu grupo.


Exercício 2

HttpClienteAutenticado e GerenciadorRenovacaoToken: autorização transparente

Um token expirado nunca deve interromper o fluxo do usuário

Imagine que o usuário do aplicativo de delivery abre a tela de cardápio após uma hora sem usar o aplicativo. O access token expirou. Se o aplicativo simplesmente retornar um erro 401 e redirecionar para a tela de login, o usuário fica confuso — ele acabou de abrir o aplicativo, não fez nada de errado, e de repente está na tela de login novamente. Esse comportamento é tecnicamente correto, mas péssimo em termos de experiência. A solução certa é tornar a renovação do token completamente transparente: o interceptor HTTP detecta que o token está prestes a expirar, renova-o silenciosamente antes de fazer a requisição, e o usuário nunca percebe o que aconteceu. Este exercício implementa exatamente esse mecanismo, com um detalhe de concorrência que não pode ser ignorado.

O problema da concorrência é o seguinte: quando o usuário abre o aplicativo após o token expirar, a tela inicial frequentemente dispara várias requisições em paralelo — cardápio, endereços salvos, pedidos recentes, promoções. Se cada uma dessas requisições detectar independentemente que o token expirou e tentar renová-lo, você terá dez chamadas simultâneas ao endpoint /token do servidor de autorização, das quais nove retornarão erro porque o refresh token de uso único já foi consumido pela primeira. O resultado é que nove das dez telas retornam erro desnecessariamente. O GerenciadorRenovacaoToken existe para resolver exatamente esse problema.

O primeiro componente a implementar é o GerenciadorRenovacaoToken. Ele possui um único campo privado: Future<void>? _pendente. O método público renovar recebe uma função Future<void> Function() fn e implementa a seguinte lógica: _pendente ??= fn().whenComplete(() => _pendente = null); return _pendente!;. Esse padrão funciona porque Future em Dart é multicast — múltiplos await no mesmo objeto Future recebem o mesmo resultado quando ele completar. Quando a primeira requisição chama renovar, o campo _pendente é nulo, então a função fn é chamada e o Future resultante é armazenado em _pendente. As demais nove requisições que chegam em seguida encontram _pendente já preenchido, então o operador ??= não executa fn novamente — elas simplesmente fazem await no mesmo Future já em andamento. Quando a renovação terminar, todas as dez requisições recebem o resultado simultaneamente e prosseguem com o token renovado. O whenComplete garante que _pendente seja limpo ao final, independentemente de sucesso ou falha.

O segundo componente é a exceção SessaoExpiradaException. Implemente-a como class SessaoExpiradaException implements Exception com um campo final String mensagem, um construtor const SessaoExpiradaException(this.mensagem) e um override de toString() que retorna uma representação legível. Usar const no construtor é importante porque essa exceção pode ser instanciada em locais críticos de performance, e o compilador Dart pode otimizá-la.

O terceiro e principal componente é o HttpClienteAutenticado, que deve estender http.BaseClient. Ele recebe por construtor três colaboradores: TokenRepositorio tokenRepositorio, Future<void> Function() renovarToken (a função de renovação fornecida pela camada de autenticação), e http.Client? clienteInterno com padrão http.Client(). O GerenciadorRenovacaoToken deve ser instanciado internamente — não injetado pelo construtor — pois ele é um detalhe de implementação que não deve vazar para os consumidores da classe.

A implementação do método send deve seguir a sequência exata a seguir. Primeiro, verifica proativamente se o access token é válido com await _tokenRepositorio.accessTokenEstaValido(); se não for, chama await _gerenciador.renovar(_renovarToken). Essa verificação proativa é preferível à verificação reativa porque evita o round-trip de rede de uma requisição que inevitavelmente falharia — o custo de verificar a validade localmente é negligenciável comparado ao custo de uma requisição que retorna 401 e precisa ser repetida. Segundo, lê o access token com await _tokenRepositorio.lerAccessToken(); se o resultado for nulo mesmo após a renovação, lança um StateError com mensagem descritiva, pois essa situação indica um bug na implementação da renovação. Terceiro, adiciona o header Authorization: Bearer $accessToken na requisição. Quarto, delega para await _inner.send(req). Quinto, verifica se resposta.statusCode == 401; se sim, chama await _tokenRepositorio.limparTodosOsTokens() e lança const SessaoExpiradaException('Sessão encerrada. Faça login novamente.'), pois um 401 que persiste após a renovação indica que o refresh token também foi revogado ou expirou. Por fim, retorna a resposta. Sobrescreva também o método close() para chamar _inner.close() e então super.close(), garantindo que o cliente interno seja devidamente descartado.

O quarto componente é o SessaoNotifier, que deve estender ChangeNotifier. Antes de definir a classe, declare o enum EstadoSessao com os valores verificando, autenticacaoBiometricaNecessaria, autenticado e naoAutenticado. Para fins deste exercício, você pode declarar interfaces mínimas para os colaboradores: IAutenticacaoRepositorio com os métodos abstratos necessários, além de usar as classes concretas TokenRepositorio e BiometriaServico que você implementou no exercício anterior.

O estado inicial do SessaoNotifier deve ser verificando. O campo EstadoSessao _estado deve ser privado com um getter público. Um campo String? _mensagemErro deve expor a última mensagem de erro disponível. O campo ModeloUsuarioAutenticado? _usuario deve guardar os dados do usuário logado.

O método verificarSessaoInicial() executa a máquina de estados inicial: verifica temSessaoAtiva() no repositório de tokens; se falso, muda _estado para naoAutenticado e notifica; se verdadeiro, verifica se a biometria está disponível; se sim, muda para autenticacaoBiometricaNecessaria e notifica; se não, chama o método privado _restaurarSessao(). O método confirmarBiometria() solicita autenticação biométrica com o motivo 'Confirme sua identidade para acessar o delivery'; se bem-sucedida, chama _restaurarSessao(); se não, define _mensagemErro e notifica. O método privado _restaurarSessao() chama renovarToken() no repositório de autenticação; em caso de sucesso, muda para autenticado e notifica; em caso de qualquer exceção, chama limparTodosOsTokens() e muda para naoAutenticado. Os métodos fazerLogin() e fazerLogout() implementam as transições correspondentes, com fazerLogout() chamando limparTodosOsTokens() antes de mudar o estado.

O quinto componente é uma TelaPrincipal que usa Consumer<SessaoNotifier> e exibe conteúdo diferente via switch sobre notifier.estado: para verificando, exibe um CircularProgressIndicator centralizado; para autenticacaoBiometricaNecessaria, exibe um botão “Usar biometria” que chama notifier.confirmarBiometria(); para autenticado, exibe o texto 'Bem-vindo, ${notifier.usuario?.nome}' seguido de um botão “Sair” que chama notifier.fazerLogout(); para naoAutenticado, exibe um botão “Fazer login” que chama notifier.fazerLogin().

O HttpClienteAutenticado não deve ter conhecimento de nenhuma lógica de negócio do delivery. Ele deve funcionar como um cliente HTTP genérico autenticado, capaz de ser usado em qualquer projeto que precise de renovação transparente de tokens. Não importe modelos de domínio do delivery dentro dessa classe.

Reflita sobre o que aconteceria se o GerenciadorRenovacaoToken não existisse e cada instância de HttpClienteAutenticado tentasse renovar o token de forma independente. Quantas chamadas ao endpoint /token seriam disparadas quando dez requisições chegam simultaneamente com um token expirado? Por que a verificação proativa da validade do token no início do método send é preferível à verificação reativa — ou seja, por que é melhor verificar antes de tentar do que tentar e verificar somente quando receber 401?

O campo headers de um http.BaseRequest é um Map<String, String> mutável somente enquanto a requisição ainda não foi enviada. No momento em que _inner.send(req) é chamado, a requisição é “fechada” pelo framework e qualquer tentativa posterior de modificar os headers lança um StateError. Por isso, o ponto correto para injetar o header Authorization: Bearer é imediatamente antes de chamar _inner.send(req), que é exatamente a sequência descrita no enunciado. Qualquer inversão dessa ordem produzirá um erro em tempo de execução que pode ser difícil de depurar.

O que deve ser entregue: um arquivo chamado g_ex2.dart, onde g é o nome do seu grupo.


Exercício 3

Lambda Authorizer + roteamento protegido: autenticação ponta a ponta

A segurança começa no servidor e termina na rota

Nos dois exercícios anteriores você construiu a fundação da autenticação no cliente: armazenamento seguro, biometria, interceptor HTTP transparente e máquina de estados de sessão. Agora você vai fechar o ciclo integrando a autenticação em toda a pilha. No servidor, você implementará um Lambda Authorizer em Python que valida o JWT antes de qualquer requisição chegar à lógica de negócio, e uma Lambda de negócio que extrai o ID do usuário do contexto propagado pelo authorizer. No cliente Flutter, você construirá o roteamento protegido com go_router que redireciona automaticamente com base no estado de sessão. Ao final deste exercício, você terá uma implementação de ponta a ponta onde nenhuma rota protegida pode ser acessada sem um JWT válido, tanto no cliente quanto no servidor.

A importância de validar o JWT no servidor, e não apenas no cliente, não pode ser subestimada. Um atacante que conseguisse bypassar as proteções do cliente — por exemplo, manipulando o armazenamento local em um dispositivo com root — ainda encontraria o Lambda Authorizer como barreira. A validação no servidor é a última linha de defesa e deve ser tratada como tal: qualquer requisição que chegue ao API Gateway sem um JWT válido deve ser rejeitada antes de consumir recursos computacionais da Lambda de negócio.

Este exercício exige dois artefatos: g_ex3.py, contendo o código Python para o Lambda Authorizer e a Lambda de listagem de pedidos; e g_ex3.dart, contendo o roteamento protegido com go_router, o SessaoNotifier completo, a tela de login e a tela de pedidos do usuário.

Parte Python — g_ex3.py

O arquivo Python deve conter dois handlers claramente delimitados por comentários de seção.

O primeiro handler implementa o Lambda Authorizer chamado delivery-authorizer. Ele deve executar a seguinte sequência de validações. Primeiro, extrai o token do campo event.get("authorizationToken", ""). Se o valor obtido não começa com "Bearer ", retorna imediatamente uma política Deny para event["methodArn"] com principalId = "desconhecido". Esse campo authorizationToken é específico para autorizadores do tipo TOKEN configurados no API Gateway — não confunda com o campo event["headers"]["Authorization"], que é usado em autorizadores do tipo REQUEST. Segundo, remove o prefixo "Bearer " para obter o JWT bruto.

Implemente a função auxiliar decodificar_jwt_payload(token: str) -> dict que: divide o token pelo caractere ., verifica que há exatamente três partes, extrai partes[1] como o payload codificado em Base64URL, aplica padding com = até que o comprimento da string seja múltiplo de 4, chama base64.urlsafe_b64decode(partes[1] + padding) para decodificar, e retorna json.loads(decoded). Essa função não verifica a assinatura criptográfica — em produção você usaria a chave pública do servidor de autorização para isso, mas para os fins deste exercício a verificação das claims é suficiente para demonstrar o padrão arquitetural.

Terceiro, verifica se payload["exp"] < int(time.time()); se verdadeiro, loga f"Token expirado: sub={payload.get('sub')}" e retorna Deny. Quarto, lê JWT_ISSUER e JWT_AUDIENCE de variáveis de ambiente com os.environ; verifica se payload.get("iss") != JWT_ISSUER; se diferente, loga e retorna Deny. Quinto, verifica o audience: aud = payload.get("aud", "") pode ser uma string simples ou uma lista de strings — trate os dois casos verificando se JWT_AUDIENCE está no aud. Sexto, extrai usuario_id = payload.get("sub", "") e retorna gerar_politica("Allow", event["methodArn"], usuario_id). Todo o bloco deve estar envolto em um try/except Exception que, em caso de qualquer erro não antecipado, loga o erro e retorna uma política Deny.

Implemente a função auxiliar gerar_politica(efeito, arn_recurso, usuario_id) que retorna um dicionário Python com a estrutura exigida pelo API Gateway: principalId com o valor de usuario_id, policyDocument com uma Statement contendo Action: "execute-api:Invoke", Effect: efeito e Resource: arn_recurso, e context: {"usuarioId": usuario_id}. O campo context é o mecanismo pelo qual o usuarioId é propagado para as Lambdas de negócio downstream sem que elas precisem desempacotar o JWT novamente.

O segundo handler implementa a Lambda delivery-listar-meus-pedidos. Ela extrai usuario_id = event.get("requestContext", {}).get("authorizer", {}).get("usuarioId"). Se esse valor for nulo ou vazio, retorna uma resposta HTTP 401. Em seguida, usa a função obter_conexao() (já conhecida dos módulos anteriores, que retorna uma conexão psycopg2) para executar SELECT id, status, total, criado_em FROM pedidos WHERE usuario_id = %s ORDER BY criado_em DESC LIMIT 20 com RealDictCursor. Serializa o resultado com json.dumps([dict(p) for p in pedidos], ensure_ascii=False, default=str) e retorna uma resposta 200 com Content-Type: application/json. Trata psycopg2.OperationalError retornando 503 e qualquer outra exceção retornando 500.

Parte Dart — g_ex3.dart

O arquivo Dart deve conter quatro componentes: o GoRouter com redirect guard, o SessaoNotifier completo, a TelaLogin e a TelaMeusPedidos.

O GoRouter deve ser configurado com as rotas /splash, /login, /home, /cardapio e /meus-pedidos. O parâmetro refreshListenable deve receber a instância do SessaoNotifier, que é um ChangeNotifier. Quando o SessaoNotifier notifica uma mudança, o go_router automaticamente reavalia todos os redirects — esse mecanismo é preferível a chamar router.refresh() manualmente porque evita que o SessaoNotifier precise conhecer a existência do router, mantendo a separação de responsabilidades entre a camada de estado e a camada de navegação.

O parâmetro redirect do GoRouter deve receber (BuildContext context, GoRouterState state) e implementar a seguinte lógica: obtém o SessaoNotifier via Provider.of<SessaoNotifier>(context, listen: false); se o estado for verificando e a rota atual não for /splash, redireciona para /splash; se o estado for naoAutenticado e a rota atual não for /login nem /splash, redireciona para /login; se o estado for autenticacaoBiometricaNecessaria e a rota atual não for /splash, redireciona para /splash; se o estado for autenticado e a rota atual for /login, redireciona para /home; em todos os demais casos, retorna null — nunca lance uma exceção no redirect, pois isso derruba a navegação inteira.

O SessaoNotifier deste exercício é o do exercício anterior acrescido de um método público refreshNotifier() que chama notifyListeners() diretamente, útil para forçar uma reavaliação dos redirects em cenários específicos. O SessaoNotifier deve ser configurado com refreshListenable: sessaoNotifier no GoRouter, conforme descrito acima.

A TelaLogin é um StatefulWidget com um ElevatedButton “Entrar com sua conta” que chama Provider.of<SessaoNotifier>(context, listen: false).fazerLogin(). Enquanto o estado for verificando, o botão deve exibir um CircularProgressIndicator compacto em vez do texto. Se SessaoNotifier.mensagemErro não for nulo, use addPostFrameCallback para exibir um SnackBar com o conteúdo do erro após o frame ser construído. A tela deve centralizar um FlutterLogo de tamanho 80 acima do botão, representando o logotipo do delivery.

A TelaMeusPedidos é um StatefulWidget que em initState chama um método assíncrono que usa o HttpClienteAutenticado para fazer uma requisição GET para ${AppConfig.urlBaseApi}/meus-pedidos. Declare uma classe AppConfig com a constante urlBaseApi = 'https://{api-id}.execute-api.sa-east-1.amazonaws.com/{stage}'. Declare também um modelo mínimo Pedido com os campos id (String), status (String), total (double) e criadoEm (String), com um construtor factory Pedido.fromJson(Map<String, dynamic> json). Popule uma lista de Pedido e exiba com ListView.builder, mostrando o id, o status em um Chip colorido conforme o status, e o total formatado como moeda. Trate SessaoExpiradaException chamando Navigator.pushReplacementNamed(context, '/login').

Reflita sobre três perguntas que envolvem a segurança ponta a ponta implementada neste exercício. Por que o Lambda Authorizer verifica tanto o iss quanto o aud em vez de apenas a expiração do token? Um token emitido pelo servidor de autorização do delivery, mas destinado a uma API de pagamentos com aud: "payment-api", poderia ser reutilizado para acessar o endpoint de pedidos do delivery se a verificação de audience estivesse ausente? Por que usar refreshListenable: sessaoNotifier no GoRouter é preferível a chamar router.refresh() manualmente em cada notifyListeners() do SessaoNotifier?

O Lambda Authorizer do tipo TOKEN recebe o token no campo event["authorizationToken"], não em event["headers"]["Authorization"]. Essa distinção é fundamental: ao configurar um authorizer no API Gateway, você escolhe entre os tipos TOKEN e REQUEST, e essa escolha determina como o token chega ao handler. Autorizadores do tipo TOKEN extraem automaticamente o valor do header Authorization e o passam em event["authorizationToken"]. Autorizadores do tipo REQUEST passam todos os headers dentro de event["headers"]. Confundir os dois casos resulta em todos os tokens sendo rejeitados silenciosamente, com o authorizer retornando Deny para todas as requisições sem nenhuma mensagem de erro clara.

O que deve ser entregue: dois arquivos — g_ex3.py (contendo o Lambda Authorizer e a Lambda de listagem de pedidos do usuário) e g_ex3.dart (contendo o GoRouter com redirect guard, o SessaoNotifier completo, a TelaLogin e a TelaMeusPedidos), onde g é o nome do seu grupo.