Architecture
Authentication

Authentication

lk-wiz uses AWS Cognito as its identity provider, supporting both Google social login and email/password, with reCAPTCHA on signup and a mandatory admin-approval gate.

Cognito user pool

A single Cognito user pool backs both the web app and the MCP server. It is configured with:

  • Google as a social identity provider.
  • Email/password with reCAPTCHA Enterprise protection on registration.
  • Lambda triggers that customize the sign-up and token lifecycle.

Lambda triggers

TriggerPurpose
Pre-signupAuto-verifies social (Google) signups; email/password signups follow the standard verification flow.
Post-confirmationProvisions the user in DynamoDB and notifies admins via Slack that a new user is pending approval.
Pre-token-generationInjects email, custom:role, and custom:approved claims into both the ID and access tokens. It only stamps the current approval state onto the token — it does not block issuance, so an unapproved user still receives a fully valid (signed, non-expired) token.

Approval gate

New users exist in Cognito immediately but are marked approved: false in DynamoDB until a platform admin approves them. The pre-token-generation trigger reflects this state into the custom:approved claim on each token.

Known gap / security note. The custom:approved approval gate is not enforced on the REST API. See Approval is not enforced on REST below — an authenticated-but-unapproved user can call /api/* REST routes today.

JWT claims & authorization

Access tokens carry the user's identity (sub, email) plus approval (custom:approved) and role (custom:role) information.

The deployed API Gateway authorizer is a native Amazon Cognito JWT authorizer (aws_apigatewayv2_authorizer.lkwiz_jwt, authorizer_type = "JWT", wired to every require_auth = true route). On each request it validates only:

  • the token signature (against the Cognito JWKS),
  • the issuer (iss) — the configured Cognito user pool, and
  • the audience (aud) — the web app client ID.

It does not inspect custom claims, so it cannot evaluate custom:approved. A valid, non-expired token from any user in the pool — approved or not — passes it. The forwarded claims are then read by the domain handlers, which apply workspace-level RBAC (owner/editor/viewer) before returning data.

The same token model is reused by the MCP server; see the MCP authentication docs.

Approval is not enforced on REST

Security note — read this before assuming REST is approval-gated.

The REST API is protected by the native Cognito JWT authorizer described above, which validates signature/issuer/audience only. It does not check custom:approved. Consequently, an authenticated-but-unapproved user can call /api/* REST routes today.

A Lambda approval-authorizer (authorizer_handlerauthorizer_service.authorize) that does enforce custom:approved == "true" exists in the codebase, but it is built and unwired: the function is declared in the Lambda handler map, yet no aws_apigatewayv2_authorizer resource references it, so it is attached to zero routes.

Approval is currently enforced only in two places:

  • WebSocket collaboration ticketscollab_service._verify_ticket_valid rejects a $connect whose ticket was minted with approved != "true".
  • Admin operationsadmin_service gates admin-only actions in its own service layer.

The frontend also hides the app from unapproved users client-side (the "Awaiting Approval" screen decodes custom:approved from the token), but that is a UX affordance, not a server-side security control.