Skip to content

Repository files navigation

AWS Lambda Authorizer

Custom Lambda Authorizer para o Amazon API Gateway que valida tokens JWT emitidos pelo Keycloak (OpenID Connect) e devolve um contexto de autorização para os serviços downstream.

A aplicação é escrita em NestJS, empacotada para Node.js 20 na AWS Lambda, e instrumentada com AWS X-Ray e Sentry.


Índice


Visão geral

O que este serviço faz

  1. Recebe um token JWT (via header Authorization: Bearer … ou cookie accessToken).
  2. Valida o audience contra a lista de clientes permitidos (KEYCLOAK_ALLOWED_CLIENTS).
  3. Faz token introspection no Keycloak para garantir que o token ainda está ativo (não revogado/expirado no IdP).
  4. Verifica a assinatura JWT com as chaves públicas do realm (JWKS), algoritmo RS256.
  5. Em caso de sucesso, retorna { isAuthorized: true, context: { … } } para o API Gateway propagar aos backends.
  6. Em caso de falha, dispara UnauthorizedException / { isAuthorized: false }, resultando em 401 no client.

Principais recursos

Área Detalhes
Keycloak / OIDC Validação JWT via JWKS; introspection no endpoint OpenID Connect; suporte a múltiplos realms e clientes
Entrada do token Header Authorization ou cookie accessToken
Segurança Checagem de aud, issuer (iss), assinatura RS256 e status ativo via introspection
Modos de execução Lambda Authorizer (REQUEST) e HTTP API (/verify, /health) no mesmo handler
Observabilidade AWS X-Ray (subsegmentos) + Sentry (spans, exceções, wrapper serverless)
Deploy Bundle otimizado com esbuild + ZIP (dist.zip) via pipeline GitLab

Arquitetura

O entrypoint único é src/main.ts (handler). Ele classifica o evento recebido e encaminha para o bootstrap adequado:

                    ┌─────────────────────────┐
                    │   Lambda handler        │
                    │   (main.ts + Sentry)    │
                    └───────────┬─────────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
   Authorizer event      HTTP event        NODE_ENV=build-test
   (type REQUEST/JWT     (requestContext.http)   → { isAuthorized: false }
    ou methodArn)
              │                 │
              ▼                 ▼
   bootstrap-guard.ts    handler-http.ts
   (Nest Application     → bootstrap-http.ts
    Context + Facade)      (Nest + Fastify
                           + @fastify/aws-lambda)
              │                 │
              └────────┬────────┘
                       ▼
              AuthorizerFacade.validateToken()
                       │
                       ▼
              AuthorizerService.verifyToken()

Camadas

Camada Responsabilidade
Handler / Bootstrap Detecta tipo de evento; sobe contexto Nest ou app Fastify
Controller (AuthorizerController) Endpoint HTTP GET /verify
Facade (AuthorizerFacade) Orquestra validação, monta o context, captura erros no Sentry
Service (AuthorizerService) Audience, introspection Keycloak, JWKS + jwt.verify
Observabilidade SentryService, XRayService
Config @nestjs/config com authorizerConfig, sentry, xray

Extração do token (modo Authorizer)

Em bootstrap-guard.ts:

  1. Authorization / authorization (remove o prefixo Bearer ).
  2. Se não houver header, lê o cookie accessToken=… de Cookie / cookie.

Fluxo de validação

Implementado em AuthorizerService.verifyToken:

token
  │
  ├─ 1. jwt.decode (completo) → exige header.kid
  │
  ├─ 2. verifyTokenAudience
  │      aud (string ou array) deve intersectar KEYCLOAK_ALLOWED_CLIENTS
  │
  ├─ 3. verifyTokenIntrospect
  │      POST {KEYCLOAK_BASE_URL}/realms/{realm}/protocol/openid-connect/token/introspect
  │      body: token + client_id (aud permitido) + client_secret (por azp)
  │      exige introspect.active === true
  │
  └─ 4. jwt.verify (RS256)
         chave via JWKS do realm
         audience = payload.azp
         issuer   = payload.iss

Resolução do realm

Ordem em getRealmFromPayload:

  1. Claim realm no payload (se string não vazia).
  2. Extração de iss no padrão /realms/{realm}.
  3. Fallback: KEYCLOAK_REALM.

JWKS

  • URI: {KEYCLOAK_BASE_URL}/realms/{realm}/protocol/openid-connect/certs
  • Clients JWKS são cacheados em memória por realm (Map).

Client secrets (introspection)

Mapeados em authorizer.config.ts a partir do azp do token:

Client (azp) Variável de ambiente
scrow-web SCROW_WEB_CLIENT_SECRET
portal-web PORTAL_WEB_CLIENT_SECRET

Clientes listados em KEYCLOAK_ALLOWED_CLIENTS sem secret configurado falharão na introspection.


Respostas

Sucesso (autorizado)

{
  "isAuthorized": true,
  "context": {
    "userId": "user-uuid",
    "email": "user@email.com",
    "name": "User Name",
    "roles": ["role-a", "role-b"],
    "realm": "media",
    "tenants": [],
    "allIsps": false,
    "firstName": "User",
    "lastName": "Name",
    "legacyId": ""
  }
}

Campos do context (origem no JWT):

Campo Claim / origem
userId sub
email email
name name
roles resource_access[azp].rolesrealm_access.roles
realm realm
tenants tenants
allIsps all_isps
firstName given_name
lastName family_name
legacyId legacy_id

No API Gateway (HTTP API simple response / REQUEST authorizer), esse payload é usado para permitir a chamada e injetar o context na integração com o backend.

Falha (não autorizado)

A facade lança UnauthorizedException. Em modo authorizer, o efeito prático para o client é 401. Em modo HTTP, o filtro de exceção responde com corpo no formato:

{
  "statusCode": 401,
  "error": "UnauthorizedException",
  "message": "",
  "timestamp": "",
  "path": "/verify",
  "isAuthorized": false
}

Erros comuns tratados como não autorizado:

  • Token ausente
  • Audience inválido
  • Token inativo (introspection)
  • Assinatura / issuer / expiração inválidos (jwt.verify)
  • Falha ao obter chave JWKS (kid inválido)

Estrutura do projeto

authorizer/
├── build/                          # Sistema de build/empacotamento Lambda (esbuild + ZIP)
│   ├── index.mjs                   # Entrypoint do processo de deploy
│   ├── build.mjs                   # Compilação TS + bundle
│   ├── test.mjs                    # Smoke test do bundle
│   ├── package.mjs                 # Gera dist.zip
│   ├── deploy.mjs                  # Orquestra build → test → package
│   ├── app.config.json
│   └── utils/
├── events/
│   └── test.json                   # Evento de exemplo para `sam local invoke`
├── src/
│   ├── main.ts                     # Handler Lambda (roteamento Guard vs HTTP)
│   ├── app.module.ts
│   ├── health.controller.ts        # GET /health
│   ├── authorizer/
│   │   ├── authorizer.module.ts
│   │   ├── authorizer.service.ts   # Validação JWT / Keycloak
│   │   ├── authorizer.config.ts    # Env → config + client secrets
│   │   ├── authorizer.interface.ts
│   │   ├── facade/
│   │   │   └── authorizer.facade.ts
│   │   └── __tests__/
│   ├── presentation/
│   │   └── controllers/
│   │       └── authorizer.controller.ts   # GET /verify
│   ├── common/observability/
│   │   ├── sentry/
│   │   └── xray/
│   ├── shared/
│   │   ├── config/
│   │   │   ├── bootstrap/
│   │   │   │   ├── bootstrap-guard.ts     # Modo Authorizer
│   │   │   │   └── bootstrap-http.ts      # Modo HTTP (Fastify)
│   │   │   └── handlers/
│   │   │       └── handler-http.ts
│   │   └── exceptions/
│   └── utils/
│       ├── boolean.util.ts
│       └── number.util.ts
├── template.example.yaml           # Modelo SAM (sem segredos)
├── template.yaml                   # SAM local (gitignored — criar a partir do example)
├── .env.example
├── .gitlab-ci.yml
├── package.json
└── README.md

Pré-requisitos

  • Node.js 20+
  • npm (ou yarn)
  • AWS SAM CLI (para invoke local)
  • Docker (necessário para sam local invoke)
  • Acesso a um Keycloak com realm/clientes configurados e secrets de client (confidential) para introspection

Configuração

1. Dependências

npm install

2. Variáveis de ambiente

Copie .env.example para .env na raiz e preencha:

cp .env.example .env

Keycloak

Variável Obrigatória Descrição
KEYCLOAK_BASE_URL Sim URL base do Keycloak (ex.: https://login.example.com)
KEYCLOAK_REALM Sim* Realm padrão (fallback se o token não trouxer realm/iss parseável)
KEYCLOAK_ALLOWED_CLIENTS Sim Lista CSV de aud permitidos (ex.: scrow-web,portal-web,watch-web)
SCROW_WEB_CLIENT_SECRET Condicional Secret do client scrow-web (introspection)
PORTAL_WEB_CLIENT_SECRET Condicional Secret do client portal-web (introspection)

*Usado quando o realm não pode ser inferido do token.

Sentry

Variável Descrição
SENTRY_DSN DSN do projeto no Sentry
SENTRY_ENVIRONMENT Ambiente reportado (ex.: dev, qa, prod)
SENTRY_TRACES_SAMPLE_RATE Taxa de sampling de traces (ex.: 0.1)
SENTRY_PROFILES_SAMPLE_RATE Taxa de profiling (ex.: 0)
SENTRY_SEND_DEFAULT_PII Enviar PII padrão (true/false)
SENTRY_ENABLE_LOGS Habilitar logs no Sentry
SENTRY_ENABLED Liga integração Nest (true apenas quando desejado em ambientes não-local/development)
SENTRY_DEBUG Debug do SDK
SENTRY_SERVICE_NAME Nome do serviço (serverName)
SENTRY_RELEASE Identificador de release
APP_VERSION Versão da aplicação

O módulo Sentry só ativa a integração Nest completa quando SENTRY_ENABLED=true, há SENTRY_DSN e NODE_ENV não é development nem local. O handler Lambda ainda é envolvido por @sentry/serverless (AWSLambda.wrapHandler).

AWS X-Ray

Variável Descrição Default
AWS_XRAY_ENABLED Liga/desliga X-Ray true
AWS_XRAY_ENVIRONMENT Ambiente (ou usa NODE_ENV)
AWS_XRAY_DAEMON_ADDRESS Endereço do daemon (se aplicável) vazio

Setup de X-Ray é ignorado quando NODE_ENV é test ou local. Em execução Lambda com tracing ativo no template SAM, o subsegmento KeycloakVerifyToken é criado durante a verificação do JWT.

Outras

Variável Descrição
NODE_ENV development, local, test, build-test, etc.
PORT Porta do HTTP local via bootstrap Fastify (default 3001)

3. Template SAM

template.yaml está no .gitignore. Crie a partir do exemplo:

cp template.example.yaml template.yaml

Ajuste em template.yaml:

  • Variáveis de ambiente da função (Keycloak, Sentry, secrets dos clients).
  • Runtime nodejs20.x, timeout, memória e Tracing: Active.
  • Handler: dist/main.handler (build Nest) ou o path gerado pelo fluxo build:lambda, conforme o processo de deploy usado.

Não versionar secrets no repositório. Em ambientes reais os valores vêm dos Secrets Manager / variáveis da pipeline (ver CI/CD).


Scripts disponíveis

Definidos em package.json:

Script Comando Descrição
build nest build Compila TypeScript para dist/
test jest Suite de testes unitários
build:lambda node build/index.mjs Build + teste do bundle + empacota dist.zip
sam:build sam build --no-cached Build SAM
sam:invoke sam local invoke AuthorizerFunction --event events/test.json --skip-pull-image Invoca a Lambda localmente
sam:invoke-logs idem + --log-file sam-local.log --debug Invoke com logs detalhados
sam:build-and-invoke buildsam:buildsam:invoke Fluxo completo de teste SAM

Documentação adicional do empacotamento: build/README.md.


Execução local

Fluxo recomendado (SAM)

  1. Configure .env / variáveis no template.yaml.
  2. Coloque um token válido em events/test.json (header Authorization ou cookie accessToken).
  3. Execute:
npm run build
npm run sam:build
npm run sam:invoke

Ou em um único passo:

npm run sam:build-and-invoke

Isso simula um evento de REQUEST authorizer do API Gateway contra a função AuthorizerFunction.

Build Nest apenas

npm run build

Saída em dist/ (handler dist/main.handler conforme template SAM típico).


Evento de teste (SAM)

Arquivo: events/test.json.

Formato mínimo de um REQUEST authorizer:

{
  "type": "REQUEST",
  "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef/prod/GET/protected",
  "headers": {
    "authorization": "Bearer <JWT>",
    "cookie": "accessToken=<JWT>"
  }
}

O handler identifica authorizer quando:

  • event.type é REQUEST ou JWT, ou
  • existe event.methodArn.

Eventos HTTP (API Gateway HTTP API) são detectados por event.requestContext.http e seguem o caminho Fastify (/verify, /health).


Endpoints HTTP

Disponíveis quando o evento é tratado como HTTP (HttpApi / Fastify):

Método Path Descrição
GET /health Health check — responde Healthy API
GET /verify Valida o Bearer token do header e retorna o mesmo payload da facade

Exemplo:

GET /verify
Authorization: Bearer <JWT>

No template.example.yaml / template.yaml locais, eventos SAM podem expor rotas como /verify (HttpApi) e /protected (API de teste sem authorizer acoplado no template de exemplo).


Observabilidade

Sentry

  • Wrapper do handler: @sentry/serverless (AWSLambda.wrapHandler) com flushTimeout: 2000.
  • Spans de aplicação: AuthorizerFacade.validateToken (operation: authorizer.validateToken).
  • Exceções capturadas na facade e em falhas de JWKS / audience.
  • Filtro global SentryExceptionFilter registrado pelo módulo.

AWS X-Ray

  • Tracing da função: Tracing: Active no template SAM.
  • Subsegmento KeycloakVerifyToken durante jwt.verify.
  • Captura global de HTTP/HTTPS quando X-Ray está habilitado.

Build e empacotamento Lambda

O diretório build/ gera um artefato otimizado para deploy:

npm run build:lambda

Fluxo interno:

  1. Compilação TypeScript
  2. Bundle com esbuild
  3. Smoke tests do handler
  4. Empacotamento em dist.zip
  5. Manifeste de build

Objetivo: reduzir o tamanho do pacote (de dezenas de MB para centenas de KB no ZIP) e validar o handler antes do deploy. Detalhes: build/README.md.


CI/CD

Pipeline GitLab (.gitlab-ci.yml) usa o componente compartilhado:

include:
  - component: $CI_SERVER_FQDN/watchbrasil/infrastructure/pipelines/typescript-lambda-nest-template/pipelines@~latest

Disparada em branches e merge requests. O template de pipeline tipicamente:

  1. Instala dependências e roda testes/build.
  2. Gera o artefato Lambda (dist.zip / fluxo Nest Lambda).
  3. Publica na função correspondente ao ambiente, lendo secrets do AWS Secrets Manager.

Ambientes

Nomes configurados na pipeline:

Ambiente Lambda Secret (Secrets Manager)
Development authentication-authorizer-dev-watch authentication-authorizer/dev/watch
QA authentication-authorizer-qa-watch authentication-authorizer/qa/watch
Preproduction authentication-authorizer-preprod-watch authentication-authorizer/preprod/watch
Production authentication-authorizer-prod-watch authentication-authorizer/prod/watch

Testes

npm test

Cobertura relevante:

  • src/authorizer/__tests__/authorizer.facade.spec.ts — sucesso, token vazio, erros → UnauthorizedException, montagem do context
  • src/authorizer/__tests__/authorizer.service.spec.ts — regras de verificação do service
  • src/common/observability/sentry/__tests__/sentry.service.spec.ts
  • src/health.controller.spec.ts

Jest está configurado em package.json (rootDir: src, *.spec.ts).


Troubleshooting

Sintoma Possível causa O que verificar
Missing token / 401 imediato Header/cookie ausente ou malformado Authorization: Bearer … ou cookie accessToken=
Invalid token audience aud não está em KEYCLOAK_ALLOWED_CLIENTS CSV de clients e claim aud do JWT
Inactive token Token revogado/expirado no Keycloak Introspection; clock skew; logout/session
Erro de introspection / secret Secret ausente ou client errado SCROW_WEB_CLIENT_SECRET / PORTAL_WEB_CLIENT_SECRET alinhados ao azp
Falha JWKS / getSigningKey kid desconhecido ou URL de realm errada KEYCLOAK_BASE_URL, realm no iss, endpoint /certs
SAM não sobe Docker/SAM/template Docker rodando; template.yaml presente; npm run build antes do sam build
Bundle / handler inválido Build incompleto npm run build:lambda e logs em build/
Sentry silencioso localmente Feature flag / NODE_ENV SENTRY_ENABLED, SENTRY_DSN, ambiente ≠ local/development para integração Nest

Stack

  • Runtime: Node.js 20.x
  • Framework: NestJS 11 + Fastify (@nestjs/platform-fastify, @fastify/aws-lambda)
  • Auth: jsonwebtoken, jwks-rsa, introspection Keycloak
  • Config: @nestjs/config
  • Observabilidade: @sentry/nestjs, @sentry/serverless, aws-xray-sdk-core
  • IaC local: AWS SAM (template.yaml)
  • CI: GitLab CI + componente typescript-lambda-nest-template

About

Custom Lambda Authorizer para o Amazon API Gateway que valida tokens JWT emitidos pelo Keycloak (OpenID Connect) e devolve um contexto de autorização para os serviços downstream. A aplicação é escrita em NestJS, empacotada para Node.js 20 na AWS Lambda, e instrumentada com AWS X-Ray e Sentry.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages