Skip to content

Switch CAPE API to native Cognito User Pools authorizer #352

Description

@mehalter

Summary

Replace the custom Lambda API Gateway authorizer for the CAPE common API with a
native API Gateway Cognito User Pools authorizer, so AWS handles JWT
signature verification, expiry, and JWKS rotation instead of homegrown code.

Motivation

The current default authorizer (assets/api/authz/default_apigw_authorizer.py)
base64url-decodes the JWT payload and maps claims, but it does not verify the
token signature
and effectively allows all callers. That is a security gap and
ongoing maintenance burden. A native Cognito User Pools authorizer:

  • verifies the token against the user pool (signature, expiry, token_use),
  • removes the need to maintain JWT verification / JWKS handling in Lambda,
  • reduces the risk that login breaks due to homegrown verification bugs.

The frontend already sends Authorization: Bearer <cognito access_token>, and in
this deployment the Cognito username == email, so a human-readable
triggering_user_name is available from token claims without switching to the id
token or adding scopes.

Current state (context for whoever picks this up)

The shared ApiGateway construct assumes every authorizer is a Lambda:

  • capeinfra/resources/api.py _create_api_authorizer_lambdas() iterates the
    authorizers config and, for each, builds a Lambda from
    authorizer_def["file"], a lambda role, and an API-gateway invoke role. There
    is no branch for a non-Lambda authorizer.
  • _render_spec() unconditionally sets role and uri
    (handler.function.invoke_arn) into the template kwargs per authorizer.
  • The OpenAPI template assets/api/capi/capi-openapi-301.yaml.j2 renders the
    securitySchemes block with x-amazon-apigateway-authtype: "custom" and
    authorizerUri / authorizerCredentials (lines ~2116-2152), plus a top-level
    security: list generated from the authorizer names.
  • Config: Pulumi.cape-cod-dev.yaml / Pulumi.cape-cod-public.yaml define the
    default authorizer with type: request and file: pointing at the custom
    authorizer source.
  • Handlers read identity from the custom authorizer context:
    • post_workflow_run.py reads triggering_user_id / triggering_user_name
      from event.requestContext.authorizer and writes conf.cape.
    • get_workflow_runs.py reads triggering_user_id from
      event.requestContext.authorizer.

The Cognito user pool already exists at capeinfra.meta.principals.user_pool
(see capeinfra/meta/capemeta.py), and api.py already imports the
capeinfra.meta singleton (it uses capeinfra.meta.policies), so the user pool
ARN (capeinfra.meta.principals.user_pool.arn) is reachable from the construct.

Proposed change (four sites)

  1. capeinfra/resources/api.py

    • Branch the authorizer loop on authorizer_def["type"]. For
      cognito_user_pools, skip Lambda + role creation and instead record the
      provider ARN(s) (from capeinfra.meta.principals.user_pool.arn).
    • Branch _render_spec() so a cognito authorizer emits provider_arns
      instead of role/uri.
    • Keep the existing Lambda path fully intact for any non-cognito authorizer
      (backward compatible; the construct is shared by all CAPE APIs).
  2. assets/api/capi/capi-openapi-301.yaml.j2

    • Add a securitySchemes variant for cognito_user_pools:
      x-amazon-apigateway-authtype: cognito_user_pools with
      x-amazon-apigateway-authorizer: { type: cognito_user_pools, providerARNs: [...] },
      and name: Authorization, in: header.
  3. Pulumi.cape-cod-dev.yaml and Pulumi.cape-cod-public.yaml

    • Change the default authorizer type to cognito_user_pools and drop
      file (no Lambda source needed).
  4. Handlers assets/api/capi/handlers/post_workflow_run.py and
    get_workflow_runs.py

    • Resolve identity from event.requestContext.authorizer.claims.* instead of
      the custom context:
      • triggering_user_id = claims["sub"]
      • triggering_user_name = claims.get("email") or claims.get("cognito:username") or claims.get("username")
    • Update tests/test_workflow_user_attribution.py accordingly.
    • default_apigw_authorizer.py becomes dead code; remove it (or keep only if
      we want a method-level fallback, see allow authorizer override at api method level #218).

Risks / validation

  • capeinfra/resources/api.py is a shared construct used by all CAPE APIs;
    the change must stay backward compatible for the Lambda authorizer path.
  • Changing the authorizer type on an existing API Gateway may force
    redeployment of the authorizer/API resources.
  • Must be validated with pulumi preview / a dev deploy: confirm the API accepts
    a Cognito access token, that event.requestContext.authorizer.claims contains
    sub and username/email, and that conf.cape is still populated correctly
    on trigger.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    APIpulumi configRelate to changing our cape pulumi config. Often implies changes the code that processes the configusercurityUsers/Security tasks that should happen at roughly the same time

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions