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.
![]()
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.comque 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
- O usuário abre o portal em
portal.example.com. O script da página faz uma requisição de verificação, com cookies, parahttps://embed.example.com/dashboard?dashboardId=<id>&check=1. - 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>. - 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.
- Com a sessão ativa, a verificação retorna sucesso e o portal carrega
https://embed.example.com/dashboardem um iframe. - O ALB valida o cookie, injeta o token assinado no cabeçalho
x-amzn-oidc-datae invoca a função Lambda. - A função valida a assinatura do token, confere o painel solicitado com uma lista de permissões e chama
GenerateEmbedUrlForRegisteredUser. - 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). - 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 paraportal.example.comna 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ávelCDK_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:
- No portal do Azure, acesse Microsoft Entra ID > Registros de aplicativo > Novo registro.
- Defina o URI de redirecionamento como
https://embed.example.com/oauth2/idpresponse. - 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>' - Anote o ID do aplicativo (cliente) e o ID do diretório (locatário). Você vai informá-los ao AWS CDK como
clientIdetenantIdna Etapa 2. - Configure os escopos
openid,emaileprofile.
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 comDescribeUserouListUsersantes de gerar a URL, em vez de montar o ARN diretamente. - Domínios permitidos – Como o SDK roda na página servida pelo ALB,
AllowedDomainsprecisa incluirhttps://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 prefixosha384-. - 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-OrigineAccess-Control-Allow-Credentials, o navegador bloqueia a resposta da verificação e o portal redireciona para/loginem 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
kidno 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á mensagensFalha 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çãoemaile 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
sslPolicycomoelbv2.SslPolicy.RECOMMENDED_TLS, que aceita TLS 1.2 e TLS 1.3. Para aceitar apenas TLS 1.3, useelbv2.SslPolicy.TLS13_RES. - Na distribuição do Amazon CloudFront, defina o protocolo mínimo como
TLSv1.2_2021. No AWS CDK, use a propriedademinimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021.
- No listener HTTPS do ALB, defina a propriedade
- 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
sessionTimeoutda açãoauthenticateOidcdo listener:elbv2.ListenerAction.authenticateOidc({ // ... demais propriedades do IdP sessionTimeout: cdk.Duration.minutes(600), next: elbv2.ListenerAction.forward([targetGroup]), });Alinhe esse valor ao parâmetro
SessionLifetimeInMinutesda chamadaGenerateEmbedUrlForRegisteredUser, 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:
- Expirar os cookies de sessão do ALB (
AWSELBAuthSessionCookie-*), definindo uma data de expiração no passado. - 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. - Expirar os cookies de sessão do ALB (
- Validação do token – A função deve validar a assinatura ES256 e o campo
signerdo cabeçalhox-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-ancestorslimita 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:
- Execute
cdk destroypara excluir o ALB, a função Lambda, a função do IAM, o grupo de destino e a Amazon VPC. - Esvazie e exclua o bucket do portal e exclua a distribuição do CloudFront.
- Exclua os registros do Route 53 criados para
embed.example.comeportal.example.com. - Exclua os certificados do ACM e o segredo
oidc-client-secretno Secrets Manager, se não forem usados por outros recursos. - Remova os usuários do Amazon Quick Sight criados apenas para testes.
- 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
- Referência da API GenerateEmbedUrlForRegisteredUser
- Amazon Quick Sight Embedding SDK no GitHub
- Funções Lambda como destinos de um Application Load Balancer
- Módulo aws-lambda-python-alpha do AWS CDK
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. |
