O blog da AWS

Incorpore dashboards sem pop-ups com SSO usando o Amazon Quick Sight

O Amazon Quick Sight oferece recursos de incorporação programática que permitem integrar painéis interativos diretamente nas suas aplicações. Com o SDK de Incorporação do Amazon Quick Sight no GitHub e a API GenerateEmbedUrlForRegisteredUser, você entrega experiências de análise que parecem nativas do seu portal e se integram à sua infraestrutura de identidade empresarial. Neste artigo, eu mostro como construir uma solução sem servidor que combina a incorporação programática do Amazon Quick Sight com o seu Login Único (SSO) existente. Um Application Load Balancer (ALB) cuida da autenticação baseado no protocolo OAuth 2.0 de forma transparente, e os usuários vão do portal corporativo direto para os seus painéis personalizados, sem etapas de login adicionais.


Entendendo as abordagens de incorporação do Quick Sight

O Amazon Quick Sight oferece dois modelos principais de incorporação, cada um projetado para casos de uso diferentes.

Incorporação em 1 clique

A incorporação em 1 clique é uma opção sem código: os autores incorporam painéis com um snippet de iframe. A abordagem é ideal para conteúdo público ou implantações internas rápidas, porque não exige infraestrutura de backend.

Em cenários autenticados, porém, a incorporação em 1 clique exige que o usuário estabeleça uma sessão diretamente com o Quick Sight. Como o iframe roda em um contexto de segurança separado da aplicação host, e os navegadores modernos bloqueiam cada vez mais cookies de terceiros, o navegador precisa abrir uma janela pop-up (ou redirecionar) para concluir a autenticação. O pop-up permite que o Quick Sight:

  • Estabeleça uma sessão de autenticação primária (first-party) no domínio do Quick Sight
  • Defina os cookies de sessão que o iframe incorporado pode acessar
  • Confirme a identidade do usuário sem expor credenciais ao host de incorporação

Depois da autenticação, o pop-up se fecha e o painel carrega. Embora funcione bem em muitos cenários, o pop-up inicial pode ser um problema em ambientes empresariais onde:

  • Políticas corporativas de navegador bloqueiam janelas pop-up
  • Cabeçalhos rigorosos de Content Security Policy (CSP) impedem interações entre origens
  • Os usuários já estão autenticados no SSO corporativo e não esperam um novo login
  • Requisitos regulatórios, como PCI DSS ou a Lei Geral de Proteção de Dados (LGPD), exigem um fluxo de autenticação controlado e auditável

Incorporação programática

Na incorporação programática, o navegador não negocia a autenticação com o Quick Sight. O backend gera uma URL de incorporação pré-autenticada e de uso único pela API do Quick Sight, depois de verificar a identidade do usuário no servidor com o seu SSO. Isso elimina a necessidade de pop-ups. A incorporação programática com usuários registrados oferece a integração mais completa, porque:

  • O painel aparece como parte nativa da sua aplicação
  • A segurança em nível de linha (RLS) do Quick Sight mostra a cada usuário apenas os dados que ele pode ver
  • A experiência é personalizada com base na identidade do usuário autenticado
  • Há suporte a marcadores, ações personalizadas e filtragem por parâmetros

Visão geral da solução

A arquitetura conecta o seu provedor de identidade (IdP) corporativo à API de incorporação do Quick Sight e trata a autenticação na camada de infraestrutura.

Arquitetura: portal no Amazon CloudFront, ALB com OIDC no Entra ID e AWS Lambda gerando a URL do Amazon Quick Sight.

No diagrama, o usuário acessa o portal, servido pelo Amazon CloudFront a partir de um bucket do Amazon Simple Storage Service (Amazon S3). O portal chama o ALB, que autentica o usuário no Microsoft Entra ID via OIDC e invoca a função AWS Lambda. A função gera a URL de incorporação do Amazon Quick Sight, e o portal carrega essa URL em um iframe. A seção Como funciona detalha cada etapa.

A solução tem os seguintes componentes:

  • Portal estático (Amazon S3 e Amazon CloudFront) – Página web leve, servida do Amazon S3 pelo CloudFront em portal.example.com, que hospeda o painel incorporado.
  • Application Load Balancer com autenticação OIDC – ALB em embed.example.com que autentica os usuários no IdP corporativo (neste exemplo, o Microsoft Entra ID) e repassa as declarações (claims) de identidade assinadas.
  • Função AWS Lambda – Função Python, registrada como destino (target) do ALB, que valida o token assinado pelo ALB e gera a URL de incorporação. A função não precisa rodar dentro de uma nuvem privada virtual (VPC), porque só chama APIs públicas da AWS.
  • Amazon Quick Sight – Fornece o painel incorporado com usuários registrados, RLS e o SDK de Incorporação.

O portal e o ALB ficam em subdomínios do mesmo domínio registrável (example.com). Assim, o cookie de sessão do ALB é tratado como cookie do mesmo site (same-site) quando a página do ALB é carregada no iframe do portal, e não é afetado pelo bloqueio de cookies de terceiros.

Como funciona

  1. O usuário abre o portal em portal.example.com. O script da página faz uma requisição de verificação, com cookies, para https://embed.example.com/dashboard?dashboardId=<id>&check=1.
  2. Sem sessão no ALB, a verificação falha e o portal redireciona o navegador, em uma navegação de nível superior (top-level), para https://embed.example.com/login?callback=<URL do portal>.
  3. O ALB redireciona para o Entra ID. Como o usuário já está conectado no ambiente corporativo, o SSO conclui o login sem interação, o ALB grava o cookie de sessão e a função Lambda devolve o usuário ao portal pela URL de callback.
  4. Com a sessão ativa, a verificação retorna sucesso e o portal carrega https://embed.example.com/dashboard em um iframe.
  5. O ALB valida o cookie, injeta o token assinado no cabeçalho x-amzn-oidc-data e invoca a função Lambda.
  6. A função valida a assinatura do token, confere o painel solicitado com uma lista de permissões e chama GenerateEmbedUrlForRegisteredUser.
  7. O Quick Sight retorna uma URL de incorporação de uso único, que precisa ser resgatada em até 5 minutos. A sessão resultante dura o tempo definido em SessionLifetimeInMinutes (de 15 minutos a 10 horas).
  8. A função retorna uma página HTML que usa o SDK de Incorporação para renderizar o painel.

Pré-requisitos

Antes de implementar esta solução, você precisa de:

  • Uma conta AWS com o Amazon Quick Sight na edição Enterprise
  • Um IdP compatível com OIDC; este artigo usa o Microsoft Entra ID
  • Usuários registrados no Quick Sight, mapeados ao email corporativo
  • Um domínio com zona hospedada no Amazon Route 53
  • Um certificado público do AWS Certificate Manager (ACM) para embed.example.com, na mesma Região do ALB, e outro para portal.example.com na Região us-east-1 (exigido pelo CloudFront)
  • Um bucket do Amazon S3 com uma distribuição do Amazon CloudFront servindo portal.example.com, usando o certificado do ACM da Região us-east-1 e o controle de acesso à origem (OAC), com o bloqueio de acesso público do bucket ativado
  • Node.js 18 ou superior e o AWS Cloud Development Kit (AWS CDK) v2 instalados (consulte Introdução ao AWS CDK)
  • Python 3.12 e Docker, usados pelo AWS CDK para empacotar as dependências da função (PyJWT[crypto]). Se você usa um runtime de contêiner compatível, como Finch ou Podman, defina a variável CDK_DOCKER (por exemplo, export CDK_DOCKER=finch)
  • Familiaridade com Python e TypeScript

Etapa 1: Configure o provedor de identidade para OIDC

Registre uma nova aplicação no Microsoft Entra ID:

  1. No portal do Azure, acesse Microsoft Entra ID > Registros de aplicativo > Novo registro.
  2. Defina o URI de redirecionamento como https://embed.example.com/oauth2/idpresponse.
  3. Em Certificados e segredos, crie um segredo do cliente e armazene o valor no AWS Secrets Manager com o nome oidc-client-secret. Armazene o valor como texto simples, e não como par chave/valor, porque a stack lê o segredo inteiro como client secret:
    aws secretsmanager create-secret --name oidc-client-secret --secret-string '<client-secret>'
    
  4. Anote o ID do aplicativo (cliente) e o ID do diretório (locatário). Você vai informá-los ao AWS CDK como clientId e tenantId na Etapa 2.
  5. Configure os escopos openid, email e profile.

Os endpoints OIDC do Entra ID no Microsoft Learn seguem o padrão https://login.microsoftonline.com/<tenant-id>/v2.0 (emissor) e .../oauth2/v2.0/authorize e .../oauth2/v2.0/token. O endpoint de informações do usuário é https://graph.microsoft.com/oidc/userinfo. Se você usar outro IdP, substitua esses endpoints pelos do seu provedor, publicados no documento /.well-known/openid-configuration. Com o Amazon Cognito, por exemplo, o emissor é https://cognito-idp.<region>.amazonaws.com/<user-pool-id> e os endpoints de autorização, token e informações do usuário ficam no domínio do grupo de usuários.

Etapa 2: Implante a infraestrutura com o AWS CDK

A stack desta etapa cria o ALB, a função Lambda com sua função de execução do AWS Identity and Access Management (IAM) e o destino do ALB. O portal usa o bucket do S3 e a distribuição do CloudFront listados nos pré-requisitos. A Amazon Virtual Private Cloud (Amazon VPC) é necessária apenas para o ALB. Ela tem somente sub-redes públicas e nenhum NAT gateway, porque a função Lambda roda fora da Amazon VPC. Por isso, a função de execução não precisa de permissões de interface de rede (ENI) do Amazon Elastic Compute Cloud (Amazon EC2).

Inicialize o projeto

O comando cdk init exige uma pasta vazia. Crie a pasta do projeto, inicialize o aplicativo, instale o módulo de funções Python e atualize a CLI local do AWS CDK:

mkdir embed-app && cd embed-app
cdk init app --language typescript
npm install @aws-cdk/aws-lambda-python-alpha
npm install -D aws-cdk@latest

O cdk init gera os arquivos bin/embed-app.ts e lib/embed-app-stack.ts e já instala aws-cdk-lib e constructs. O módulo @aws-cdk/aws-lambda-python-alpha mais recente pode atualizar o aws-cdk-lib para uma versão que a CLI instalada pelo cdk init não reconhece (erro Cloud assembly schema version mismatch). Por isso, o último comando atualiza a CLI do projeto. Execute os comandos do CDK com npx cdk para usar essa versão.

Substitua a stack gerada

Exclua o arquivo lib/embed-app-stack.ts e crie o arquivo lib/embed-stack.ts com o seguinte conteúdo.

lib/embed-stack.ts (TypeScript)

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as python from "@aws-cdk/aws-lambda-python-alpha";

export class EmbedStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const tenantId = this.node.tryGetContext("tenantId");
    const clientId = this.node.tryGetContext("clientId");
    const dashboardIds = this.node.tryGetContext("dashboardIds"); // "id1,id2"
    const albCertArn = this.node.tryGetContext("albCertArn");
    const qsDashboardRegion = this.node.tryGetContext("qsDashboardRegion") ?? "us-east-1";
    const qsIdentityRegion = this.node.tryGetContext("qsIdentityRegion") ?? "us-east-1";
    const embedOrigin = this.node.tryGetContext("embedOrigin");   // https://embed.example.com
    const portalOrigin = this.node.tryGetContext("portalOrigin"); // https://portal.example.com
    const entraBase = `https://login.microsoftonline.com/${tenantId}`;

    // VPC apenas para o ALB: sub-redes públicas, sem NAT gateway.
    const vpc = new ec2.Vpc(this, "Vpc", {
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [{ name: "public", subnetType: ec2.SubnetType.PUBLIC }],
    });

    const cert = acm.Certificate.fromCertificateArn(this, "AlbCert", albCertArn);
    const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", { vpc, internetFacing: true });

    // Função de execução do Lambda: logs básicos + permissão de incorporação com privilégio mínimo.
    const role = new iam.Role(this, "EmbedRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
      ],
    });
    role.addToPolicy(new iam.PolicyStatement({
      actions: ["quicksight:GenerateEmbedUrlForRegisteredUser"],
      resources: [
        ...dashboardIds.split(",").map((d: string) =>
          `arn:aws:quicksight:${qsDashboardRegion}:${this.account}:dashboard/${d}`),
        `arn:aws:quicksight:${qsIdentityRegion}:${this.account}:user/default/*`,
      ],
    }));

    // Lambda fora da VPC; as dependências do arquivo requirements.txt estão incluídas no pacote.
    const embedFn = new python.PythonFunction(this, "EmbedFn", {
      entry: "lambda",              // pasta com index.py e requirements.txt
      index: "index.py",
      handler: "lambda_handler",
      runtime: lambda.Runtime.PYTHON_3_12,
      role,
      timeout: cdk.Duration.seconds(10),
      environment: {
        ALB_ARN: alb.loadBalancerArn,
        AWS_ACCOUNT_ID: this.account,
        QUICKSIGHT_DASHBOARD_REGION: qsDashboardRegion,
        QUICKSIGHT_IDENTITY_REGION: qsIdentityRegion,
        ALLOWED_DASHBOARDS: dashboardIds,
        QUICKSIGHT_NAMESPACE: "default",
        ALLOWED_DOMAINS: `${embedOrigin},${portalOrigin}`,
        PORTAL_ORIGIN: portalOrigin,
      },
    });

    // Redirecionamento HTTP 80 -> HTTPS 443.
    alb.addListener("Http", {
      port: 80,
      defaultAction: elbv2.ListenerAction.redirect({ protocol: "HTTPS", port: "443", permanent: true }),
    });

    // Ouvinte HTTPS: autenticação OIDC, seguida de encaminhamento para o destino Lambda.
    const https = alb.addListener("Https", { port: 443, certificates: [cert] });
    const targetGroup = new elbv2.ApplicationTargetGroup(this, "EmbedTg", {
      targets: [new targets.LambdaTarget(embedFn)], // o CDK concede a permissão de invocação
    });
    https.addAction("Oidc", {
      action: elbv2.ListenerAction.authenticateOidc({
        issuer: `${entraBase}/v2.0`,
        authorizationEndpoint: `${entraBase}/oauth2/v2.0/authorize`,
        tokenEndpoint: `${entraBase}/oauth2/v2.0/token`,
        userInfoEndpoint: "https://graph.microsoft.com/oidc/userinfo",
        clientId,
        clientSecret: cdk.SecretValue.secretsManager("oidc-client-secret"),
        scope: "openid email profile",
        onUnauthenticatedRequest: elbv2.UnauthenticatedAction.AUTHENTICATE,
        next: elbv2.ListenerAction.forward([targetGroup]),
      }),
    });

    new cdk.CfnOutput(this, "AlbDns", { value: alb.loadBalancerDnsName });
  }
}

A configuração onUnauthenticatedRequest: AUTHENTICATE faz o ALB iniciar o fluxo OIDC quando não há sessão. O construct LambdaTarget registra a função no grupo de destino e concede ao ALB a permissão de invocá-la.

Atualize o arquivo bin

Substitua o conteúdo de bin/embed-app.ts para instanciar a EmbedStack com env: { account, region }. A stack usa this.account nos ARNs do IAM e nas variáveis de ambiente, por isso precisa de uma conta e de uma Região explícitas. O certificado do ACM informado em albCertArn deve estar na mesma Região da stack.

bin/embed-app.ts (TypeScript)

#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { EmbedStack } from "../lib/embed-stack";

const app = new cdk.App();

new EmbedStack(app, "EmbedStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

As variáveis CDK_DEFAULT_ACCOUNT e CDK_DEFAULT_REGION vêm do perfil ativo da AWS Command Line Interface (AWS CLI). Para fixar o destino, substitua esses valores pelo ID da conta e pela Região desejados.

Adicione o código da função e implante

Crie a pasta lambda/ na raiz do projeto com o arquivo index.py da Etapa 3 e um requirements.txt que contenha PyJWT[crypto]. Conclua a Etapa 3 antes de executar o cdk deploy, porque o deploy empacota esse código. O construct PythonFunction usa o Docker para instalar as dependências para a arquitetura da função. Em seguida, prepare o ambiente e implante a stack:

npx cdk bootstrap aws://<account-id>/<region>
npx cdk deploy \
  -c tenantId=<tenant-id> \
  -c clientId=<application-client-id> \
  -c dashboardIds=<dashboard-id> \
  -c albCertArn=arn:aws:acm:<region>:<account-id>:certificate/<cert-id> \
  -c qsDashboardRegion=us-east-1 \
  -c qsIdentityRegion=us-east-1 \
  -c embedOrigin=https://embed.example.com \
  -c portalOrigin=https://portal.example.com

Depois da implantação, crie no Amazon Route 53 um registro de alias de embed.example.com para o DNS do ALB exibido na saída AlbDns.

Etapa 3: Implemente a função AWS Lambda

Salve o código a seguir em lambda/index.py. A função valida o token assinado pelo ALB, normaliza o email do usuário, confere o painel solicitado com uma lista de permissões e trata os erros da API. Em caso de falha, a função retorna 401, 403 ou 500 em vez de expor detalhes do erro ou usar um usuário anônimo.

lambda/index.py (Python 3.12)

import json
import os
import urllib.request
from functools import lru_cache
from urllib.parse import unquote, urlparse

import boto3
import jwt  # PyJWT[crypto]
from botocore.exceptions import ClientError

REGION = os.environ["QUICKSIGHT_DASHBOARD_REGION"]
ACCOUNT_ID = os.environ['AWS_ACCOUNT_ID']
ALB_ARN = os.environ["ALB_ARN"]
ALB_REGION = ALB_ARN.split(":")[3]  # as chaves públicas são publicadas na Região do ALB
QS_IDENTITY_REGION = os.environ["QUICKSIGHT_IDENTITY_REGION"]
NAMESPACE = os.environ["QUICKSIGHT_NAMESPACE"]
ALLOWED_DASHBOARDS = set(os.environ["ALLOWED_DASHBOARDS"].split(","))
ALLOWED_DOMAINS = os.environ["ALLOWED_DOMAINS"].split(",")
PORTAL_ORIGIN = os.environ["PORTAL_ORIGIN"]

quicksight = boto3.client("quicksight", region_name=REGION)

@lru_cache(maxsize=16)
def alb_public_key(kid: str) -> str:
    """Obtém e armazena em cache a chave de assinatura do ALB pelo ID da chave."""
    url = f"https://public-keys.auth.elb.{ALB_REGION}.amazonaws.com/{kid}"
    with urllib.request.urlopen(url, timeout=2) as resp:
        return resp.read().decode("utf-8")


def verify_alb_token(token: str) -> dict:
    """Verifica o token ES256 assinado pelo ALB e retorna suas declarações (claims)."""
    # O token do ALB inclui padding em base64 e é assinado com ele; não remova o padding,
    # ou a verificação da assinatura falha. O PyJWT aceita o token como o ALB o envia.
    header = jwt.get_unverified_header(token)
    if header.get("signer") != ALB_ARN:
        raise jwt.InvalidTokenError("unexpected signer")
    return jwt.decode(token, alb_public_key(header["kid"]), algorithms=["ES256"])


def response(status: int, body: str, content_type: str = "text/plain") -> dict:
    return {
        "statusCode": status,
        "headers": {
            "Content-Type": content_type,
            # Somente o portal pode exibir esta página em um frame.
            "Content-Security-Policy": f"frame-ancestors {PORTAL_ORIGIN}",
            # A verificação do portal é uma requisição entre origens com cookies (CORS).
            "Access-Control-Allow-Origin": PORTAL_ORIGIN,
            "Access-Control-Allow-Credentials": "true",
            "Vary": "Origin",
        },
        "body": body,
    }


def safe_callback(raw: str) -> str:
    """Aceita apenas URLs de callback que apontem para a origem do portal."""
    url = unquote(raw or "")
    parsed = urlparse(url)
    if f"{parsed.scheme}://{parsed.netloc}" == PORTAL_ORIGIN:
        return url
    return PORTAL_ORIGIN + "/"


def lambda_handler(event, context):
    path = event.get("path", "")
    params = event.get("queryStringParameters") or {}

    # /login: o ALB já autenticou o usuário junto ao IdP.
    # Redireciona o navegador de volta para a página do portal que iniciou o login.
    if path == "/login":
        return {
            "statusCode": 302,
            "headers": {"Location": safe_callback(params.get("callback", ""))},
            "body": "",
        }

    if path != "/dashboard":
        return response(404, "Recurso não encontrado")

    try:
        claims = verify_alb_token(event["headers"].get("x-amzn-oidc-data", ""))
    except Exception as exc:
        print(f"Falha na validação do token: {exc}")
        return response(401, "Não autorizado")

    # Normaliza o email da mesma forma que os usuários foram registrados no Quick Sight
    # (aqui, sem espaços e em minúsculas) para que o ARN corresponda ao usuário existente.
    email = (claims.get("email") or "").strip().lower()
    # O ALB repassa os valores da query string codificados em URL.
    dashboard_id = unquote(params.get("dashboardId", ""))
    if not email or dashboard_id not in ALLOWED_DASHBOARDS:
        return response(403, "Acesso negado")

    # Verificação do portal: confirma a sessão sem gerar (e descartar) uma URL de incorporação.
    if params.get("check") == "1":
        return response(204, "")

    # Pressupõe que o nome de usuário no Quick Sight seja igual ao email normalizado (veja a observação abaixo).
    user_arn = (f"arn:aws:quicksight:{QS_IDENTITY_REGION}:{ACCOUNT_ID}"
                f":user/{NAMESPACE}/{email}")
    try:
        embed_url = quicksight.generate_embed_url_for_registered_user(
            AwsAccountId=ACCOUNT_ID,
            UserArn=user_arn,
            ExperienceConfiguration={"Dashboard": {"InitialDashboardId": dashboard_id}},
            AllowedDomains=ALLOWED_DOMAINS,
            SessionLifetimeInMinutes=600,  # duração da sessão, não validade da URL
        )["EmbedUrl"]
        return response(200, embed_page(embed_url), "text/html; charset=utf-8")
    except ClientError as err:
        code = err.response["Error"]["Code"]
        print(f"Erro do Quick Sight: {code}")
        status = 403 if code in ("ResourceNotFoundException", "AccessDeniedException") else 500
        return response(status, "Não foi possível carregar o painel")

A função embed_page, no mesmo arquivo, gera a página que renderiza o painel com o SDK de Incorporação v2. A URL é serializada com json.dumps antes de entrar no script, e o locale vai em contentOptions:

lambda/index.py (continuação)

SDK_URL = ("https://unpkg.com/amazon-quicksight-embedding-sdk@<version>"
           "/dist/quicksight-embedding-js-sdk.min.js")


def embed_page(embed_url: str) -> str:
    return f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <title>Painel do Amazon Quick Sight</title>
  <script src="{SDK_URL}" integrity="<sri-hash>" crossorigin="anonymous"></script>
  <style>body {{ margin: 0; }} #dashboard-container {{ height: 100vh; }}</style>
</head>
<body>
  <div id="dashboard-container"></div>
  <script>
    document.addEventListener("DOMContentLoaded", async () => {{
      const container = document.getElementById("dashboard-container");
      try {{
        // Usa o SDK de Incorporação do Quick Sight para incorporar o painel
        const embeddingContext = await QuickSightEmbedding.createEmbeddingContext();
        await embeddingContext.embedDashboard(
          {{
            url: {json.dumps(embed_url)},
            container,
            height: "100%",
            width: "100%",
            onChange: (changeEvent) => {{
              switch (changeEvent.eventName) {{
                case "FRAME_MOUNTED":
                  console.log("Frame do painel montado.");
                  break;
                case "FRAME_LOADED":
                  console.log("Frame do painel carregado.");
                  break;
              }}
            }},
          }},
          {{ locale: "pt-BR" }}
        );
      }} catch (err) {{
        console.error(err);
        container.innerHTML =
          `<div class="error-message">
             <h2>Painel indisponível</h2>
             <p>Não foi possível carregar o painel neste momento.</p>
           </div>`;
      }}
    }});
  </script>
</body>
</html>"""

Observações sobre o código

  • Região de identidade – Os usuários do Amazon Quick Sight ficam na Região de identidade da conta (muitas vezes us-east-1), que pode ser diferente da Região do painel. Por isso, o ARN do usuário usa QUICKSIGHT_IDENTITY_REGION, enquanto o cliente da API usa a Região do painel.
  • Nome de usuário – O código normaliza o email recebido do IdP (remove espaços e converte para minúsculas) e supõe que o nome de usuário no Quick Sight seja igual a esse valor. Registre os usuários no Quick Sight com a mesma normalização para que o ARN corresponda. Usuários federados pelo IAM costumam ser registrados como <nome-da-função>/<email>. Nesse caso, ou se o formato de registro variar, resolva o usuário com DescribeUser ou ListUsers antes de gerar a URL, em vez de montar o ARN diretamente.
  • Domínios permitidos – Como o SDK roda na página servida pelo ALB, AllowedDomains precisa incluir https://embed.example.com.
  • SDK – Fixe uma versão validada do SDK v2 no GitHub e calcule o hash de Subresource Integrity (SRI) correspondente, ou hospede o arquivo na sua própria origem do Amazon CloudFront. Para calcular o hash: curl -sL <SDK_URL> | openssl dgst -sha384 -binary | openssl base64 -A, e use o resultado com o prefixo sha384-.
  • CORS – O portal e o ALB são origens diferentes. O fato de compartilharem o domínio registrável garante o envio do cookie, mas não dispensa o CORS. Sem os cabeçalhos Access-Control-Allow-Origin e Access-Control-Allow-Credentials, o navegador bloqueia a resposta da verificação e o portal redireciona para /login em loop.
  • Validação do token – O ALB envia o token em base64url com padding e assina o conteúdo com o padding incluído. Passe o token sem alterações para a biblioteca de JWT.
  • Chaves públicas – As chaves do ALB ficam em cache por kid no escopo do módulo, o que evita uma chamada HTTPS a cada invocação.

Etapa 4: Configure o portal estático

Esta página HTML é um portal que incorpora um painel do Amazon Quick Sight. Ao carregar, ela faz uma requisição de verificação com cookies ao endpoint /dashboard, com o parâmetro check=1, para conferir se o usuário está autenticado sem gerar uma URL de incorporação. Se estiver, exibe o painel em um iframe. Se receber 401 ou 403, ou se houver falha de rede, redireciona para /login com a URL atual como callback. Para os outros erros, mostra uma mensagem na tela. Com esse processo, evitamos a necessidade de pop-ups de autenticação em contexto de iframes.

index.html

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <title>Portal de painéis do Amazon Quick Sight</title>
</head>
<body>
  <div class="container">
        <div class="hero">
            <h1>Bem-vindo ao portal de painéis do Amazon Quick Sight</h1>
            <div id="dashboard-container">
                <div class="loading">
                    <div class="loading-spinner"></div>
                </div>
                <div id="status-message" class="status-message" style="display: none;"></div>
            </div>
            <script>
                document.addEventListener('DOMContentLoaded', function() {
                    // Configuração
                    const quicksightDashboardId = '<Dashboard_ID>';
                    const baseUrl = 'https://embed.example.com';
                    const embeddingUrl = `${baseUrl}/dashboard`;
                    const dashboardEmbeddingUrl = `${embeddingUrl}?dashboardId=${encodeURIComponent(quicksightDashboardId)}`;
                    const loginUrl = `${baseUrl}/login`;
                    const dashboardContainer = document.getElementById('dashboard-container');
                    const statusMessage = document.getElementById('status-message');
                    
                    // Função para exibir mensagem de erro
                    function showError(message) {
                        statusMessage.textContent = message;
                        statusMessage.className = 'status-message error';
                        statusMessage.style.display = 'block';
                    }
                    
                    // Função para verificar a autenticação e carregar o painel
                    function checkAuthAndLoadDashboard() {
                        // Crie uma requisição `fetch` com credenciais para incluir cookies.
                        fetch(`${dashboardEmbeddingUrl}&check=1`, {
                            method: 'GET',
                            credentials: 'include',
                            headers: {
                                'Accept': 'text/html'
                            }
                        })
                        .then(response => {
                            if (response.ok) {
                                // Se a verificação for bem-sucedida, incorpore o painel.
                                dashboardContainer.innerHTML = `<iframe src="${dashboardEmbeddingUrl}" width="100%" height="850px" style="border: 0px; padding: 0px;"></iframe>`;
                                return;
                            } else if (response.status === 401 || response.status === 403) {
                                // Se não autorizado, redirecione para a página de login com uma URL de retorno.
                                const currentUrl = encodeURIComponent(window.location.href);
                                window.location.href = `${loginUrl}?callback=${currentUrl}`;
                            } else {
                                // Tratar outros erros
                                showError(`Erro ao acessar o painel: ${response.status} ${response.statusText}`);
                            }
                        })
                        .catch(error => {
                            // Se não autorizado, redirecione para a página de login com uma URL de retorno.
                            const currentUrl = encodeURIComponent(window.location.href);
                            window.location.href = `${loginUrl}?callback=${currentUrl}`;
                        });
                    }
                    
                    // Inicie a verificação de autenticação
                    checkAuthAndLoadDashboard();
                });
            </script>
        </div>
    </div>
</body>
</html>

Publique o arquivo no bucket do portal e invalide o cache do CloudFront:

Publicação do portal

aws s3 cp index.html s3://<portal-bucket>/index.html

aws cloudfront create-invalidation --distribution-id <distribution-id> --paths "/index.html"

Como portal.example.com e embed.example.com compartilham o domínio registrável, o navegador envia o cookie de sessão do ALB dentro do iframe. Sem sessão ativa, o portal redireciona o usuário para /login. Depois do login, a função devolve o usuário ao portal e o painel carrega no iframe.


Solução de problemas

  • HTTP 401 “Authorization Required” do ALB depois do login – O ALB exige que a autenticação termine em até 15 minutos depois do redirecionamento para o IdP. Esse limite não pode ser alterado. Se a página de login ficar aberta por mais tempo, recarregue o portal e faça o login novamente. Consulte Timeout.
  • Redirecionamento em loop entre o portal e /login – Verifique, no console do navegador, se a verificação foi bloqueada por CORS, e, nos logs da função, se há mensagens Falha na validação do token. As duas situações fazem a verificação falhar mesmo com uma sessão válida no ALB.
  • HTTP 403 “Acesso negado” – Confirme que o painel está em dashboardIds, que o IdP retorna a declaração email e que o usuário existe no Quick Sight com o nome igual ao email normalizado.

Considerações de segurança

  • Rede – O ALB serve conteúdo apenas por HTTPS na porta 443. A porta 80 apenas redireciona para HTTPS.
  • Transporte – O ALB e o Amazon CloudFront aceitam apenas TLS 1.2 ou superior:
    • No listener HTTPS do ALB, defina a propriedade sslPolicy como elbv2.SslPolicy.RECOMMENDED_TLS, que aceita TLS 1.2 e TLS 1.3. Para aceitar apenas TLS 1.3, use elbv2.SslPolicy.TLS13_RES.
    • Na distribuição do Amazon CloudFront, defina o protocolo mínimo como TLSv1.2_2021. No AWS CDK, use a propriedade minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021.
  • Proteção na camada de aplicação – Recomendamos associar uma web ACL do AWS WAF ao ALB com as seguintes regras:
    • Uma regra baseada em taxa (rate-based rule), que limita o número de requisições por endereço IP de origem.
    • O grupo de regras gerenciadas AWSManagedRulesCommonRuleSet (conjunto de regras comuns das AWS Managed Rules), que ajuda a bloquear padrões de ataque comuns a aplicações web.
  • Autenticação – O ALB valida a identidade no IdP antes de qualquer requisição chegar à função.
  • Duração da sessão – Por padrão, a sessão de autenticação do ALB dura 7 dias (604.800 segundos). Recomendamos reduzir esse valor com a propriedade sessionTimeout da ação authenticateOidc do listener:
    elbv2.ListenerAction.authenticateOidc({
      // ... demais propriedades do IdP
      sessionTimeout: cdk.Duration.minutes(600),
      next: elbv2.ListenerAction.forward([targetGroup]),
    });
    

    Alinhe esse valor ao parâmetro SessionLifetimeInMinutes da chamada GenerateEmbedUrlForRegisteredUser, que aceita de 15 a 600 minutos. Quando os dois valores coincidem, a sessão do ALB e a sessão do painel incorporado expiram juntas. Assim, o usuário não continua com acesso ao portal depois que o painel expira, nem mantém o painel aberto depois que a sessão do ALB termina.

  • Logout – O ALB não oferece um endpoint de logout próprio. Para encerrar a sessão, a aplicação precisa:
    1. Expirar os cookies de sessão do ALB (AWSELBAuthSessionCookie-*), definindo uma data de expiração no passado.
    2. Redirecionar o usuário para o endpoint de logout do IdP, para encerrar também a sessão no provedor de identidade. Sem essa etapa, o IdP autentica o usuário novamente de forma silenciosa na próxima requisição.

    O logout não revoga uma sessão do Amazon Quick Sight já aberta no navegador. Essa sessão continua válida até atingir o limite definido em SessionLifetimeInMinutes. Por isso, use valores curtos nesse parâmetro quando o encerramento rápido do acesso for um requisito.

  • Validação do token – A função deve validar a assinatura ES256 e o campo signer do cabeçalho x-amzn-oidc-data, conforme a documentação de autenticação do ALB. Essa etapa não é opcional.
  • Autorização – O servidor armazena a lista de painéis permitidos e o namespace. A RLS do Amazon Quick Sight restringe os dados por usuário.
  • Incorporação – As URLs de incorporação são de uso único, valem por 5 minutos e funcionam apenas nos domínios permitidos. O cabeçalho frame-ancestors limita quais páginas podem exibir o portal em um iframe.
  • Infraestrutura – A função de execução do AWS Identity and Access Management (AWS IAM) segue o princípio de privilégio mínimo, e o AWS Secrets Manager armazena o segredo do cliente.

Escalabilidade e observabilidade

O ALB e o Lambda escalam com a demanda, e o mecanismo em memória SPICE do Quick Sight atende consultas simultâneas aos painéis. Use os logs e métricas do Amazon CloudWatch para acompanhar erros 401, 403 e 500 e a latência de geração das URLs. Se precisar de rastreamento distribuído, prefira uma instrumentação mantida ativamente, como o AWS Distro for OpenTelemetry.

Limpeza

Para evitar cobranças futuras, remova os recursos criados neste tutorial:

  1. Execute cdk destroy para excluir o ALB, a função Lambda, a função do IAM, o grupo de destino e a Amazon VPC.
  2. Esvazie e exclua o bucket do portal e exclua a distribuição do CloudFront.
  3. Exclua os registros do Route 53 criados para embed.example.com e portal.example.com.
  4. Exclua os certificados do ACM e o segredo oidc-client-secret no Secrets Manager, se não forem usados por outros recursos.
  5. Remova os usuários do Amazon Quick Sight criados apenas para testes.
  6. No Entra ID, exclua o registro do aplicativo e o segredo do cliente criados na Etapa 1.

Nota: Antes de excluir, confirme que cada recurso pertence apenas a esta solução. Preserve recursos compartilhados, como a zona hospedada do Route 53 e os usuários de produção do Quick Sight.


Conclusão

Neste artigo, eu mostrei como combinar a incorporação programática do Amazon Quick Sight com a autenticação OIDC nativa do ALB para entregar painéis incorporados integrados ao seu SSO corporativo, sem pop-ups e sem logins extras. A solução é sem servidor, funciona com qualquer IdP compatível com OIDC e aplica controles de segurança em cada camada. Os custos dependem do uso de cada serviço; consulte as páginas de preços do ALB, do Lambda, do CloudFront e do Quick Sight para estimá-los.

Para começar, consulte a documentação de análise incorporada do Amazon Quick Sight, a documentação de autenticação do Application Load Balancer e a documentação de RLS do Amazon Quick Sight.

Referências adicionais


Sobre o autor

Carlos Alessandro Ribeiro é Arquiteto de Soluções Sênior na AWS, baseado em São Paulo, Brasil. Ele ajuda clientes empresariais a projetar e implementar soluções nativas da nuvem com foco em análise de dados, arquiteturas sem servidor e segurança.