Repository navigation
feat(routing): require sign-in on app.yaml paths at the load balancer - #158
Open
Alexanderamiri wants to merge 2 commits into
Open
Alexanderamiri wants to merge 2 commits into
Alexanderamiri wants to merge 2 commits into
Conversation
app.yaml `routing.login` lets an app require Cognito sign-in on chosen paths, per path choosing the pool (internal / external) and the groups that may pass. The platform ALB does the sign-in (authenticate-oidc), so simple apps and static pages need no auth code. - generate-modules.py: validate routing.login (string / mapping / list forms), emit login_rules at priorities directly below routing.priority, warn when /* is not covered (path patterns are case-sensitive). - service-routing: per protected rule, a Cognito client (nested cognito-app-client, slot login-N), a wait for app-auth-sync, and an authenticate-oidc + forward listener rule; public rules forward only. The host rule depends on them, so a new app fails closed. - cognito-app-client: optional slot; external pool falls back to its Cognito prefix domain. - app-auth-sync: requests under /apps/<team>/<app>/login-N/auth create client <team>-<app>.login-N with credentials next to the request. - identity: external pool gets the javabin-external prefix domain and a pre-token trigger (auth-claims with REGISTRY_GROUPS=false), so allowed_groups is enforced there without granting registry groups. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Terraform Plan🚧 Changes detected — Plan: 3 to add, 3 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟢 LOW Plan adds an external Cognito user pool domain, a new external auth-claims Lambda with its Cognito invoke permission, and updates the Cognito user pool (pre-token-generation hook) plus two existing Lambdas' code; 0 destroys, no database or network changes.
|
- internal pool: no self sign-up (admin-only create) and no COGNITO provider on its app clients, so login: internal admits only Google Workspace users - both pools: email changes stay pending until verified; auth-claims grants registry groups only for federated or verified emails - ALB session cookie named after slot, pool and groups, so tightening groups or reordering rules forces a fresh sign-in - external pool prefix domain kept unconditionally - protected /x/* rules also cover bare /x; broader path-bypass warning - docs: external groups are manual for now, priority spacing, denial UX, no proxying to off-platform sites Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Terraform Plan🚧 Changes detected — Plan: 3 to add, 4 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟡 MEDIUM Plan adds 3 resources (external Cognito domain, external auth-claims Lambda and its Cognito invoke permission) and updates 4 existing resources (two Cognito user pools, auth-claims Lambda code, app-auth-sync Lambda code); nothing is destroyed.
|
This was referenced Oct 24, 2026
Alexanderamiri
added a commit
that referenced
this pull request
Oct 24, 2026
## Why Part of #157. The internal pool (`javabin-internal`, `eu-central-1_Icikv3dtD`) is open to the internet today: - `AllowAdminCreateUserOnly` is `false`, so self sign-up is on. - app-auth-sync puts `COGNITO` on every client, so the hosted UI shows a sign-up link. Together, anyone can create a user in the internal pool with their own Gmail address. An app with `auth: internal` and no `groups` accepts that user. Nothing checks the email domain on the server side. The `COGNITO` entry also puts a password form in front of Google. The frosting client was hand-edited in the console to `[GoogleSAML]` to remove the form. The 03:30 UTC reconcile undoes that edit. ## What - **`identity/main.tf`** - The internal pool gets `admin_create_user_config { allow_admin_create_user_only = true }`. - The internal `auth-claims` function gets two new env vars: - `REQUIRED_EMAIL_DOMAIN = var.google_workspace_domain` (`java.no`); - `FEDERATED_IDENTITY_PROVIDERS`, set from the internal IdP list. - **New variables** `internal_identity_providers` (default `["GoogleSAML"]`) and `external_identity_providers` (default `["COGNITO"]`). - They are declared in the identity module, output from it, and passed to app-auth-sync as `INTERNAL_IDENTITY_PROVIDERS` / `EXTERNAL_IDENTITY_PROVIDERS`. - Validation: the internal list must not include `COGNITO` and must not be empty. - **app-auth-sync** - Each client gets exactly its pool's IdP list. - If a configured IdP doesn't exist in the pool, the request is skipped: an `ERROR` log and `status: error` (reconcile lists it under `error`). Nothing is created, moved or wiped, and the existing client keeps working. - Settings are resolved before the pool-switch cleanup, so a misconfigured target pool can't delete the client in the old pool. - **auth-claims**: when `REQUIRED_EMAIL_DOMAIN` is set, a token is refused unless both of these hold: - the email ends with `@java.no` (case-insensitive); - the user is federated through a listed IdP (from the `identities` attribute, or the `<Provider>_` username), or has `email_verified=true`. The user sees "Sign in with your @java.no account." Logs mask the address as `e***@java.no`. - **Review follow-ups** - app-auth-sync rejects `aws.cognito.signin.user.admin` for `pool: internal` (status `invalid`, client not created or changed). That scope would let a signed-in user call `UpdateUserAttributes` and point their email at another `@java.no` user, whose registry groups auth-claims would then grant. No live request uses it (only cupcake, with `openid email profile`). The external pool still allows it. - New `aws_cloudwatch_log_metric_filter` + `aws_cloudwatch_metric_alarm` `javabin-app-auth-sync-skipped` on `client left unchanged` in `/aws/lambda/javabin-app-auth-sync`, posting to `javabin-alerts` (Slack via slack-alert). The `javabin-alerts` topic policy gets an `AllowCloudWatchAlarmsPublish` statement scoped to this account's alarms. - `internal_identity_providers` documents that each entry is trusted to vouch for email, so it must release only Workspace accounts (Google OIDC needs an Internal-consent client, or an `email_verified`/`hd` check first). - auth-claims masks the email in the group-denial log too. - **Docs**: `docs/lambda-functions.md` (both functions) and the `docs/app-yaml-reference.md` auth section, which covers no password form or sign-up, the `@java.no` rule, and how a denial arrives as `error_description`. ## How verified - Unit tests: `python3 -m unittest discover -s terraform/lambda-src/tests` (79 OK) and `scripts/tests` (43 OK). New tests cover: - the fixed IdP list, and the reconcile reverting a console edit to the list; - env config, the missing-IdP skip on create, update, pool switch and reconcile, and an empty list; - for the domain rule: SAML users with `email_verified=false`, lookalike domains, native users with and without a verified email, other IdPs, and a pool without the rule. - `terraform fmt -check -recursive` and `terraform validate` pass. - Read-only checks against live state: - **Users:** the pool has 4 users. All are `GoogleSAML_…`, `@java.no`, `EXTERNAL_PROVIDER`, `email_verified=false`, identities provider `GoogleSAML`. All 4 pass through the federated-IdP branch. This exact shape is a unit test. - **Auth requests:** there is one, `/javabin/apps/cupcake/cupcake/auth` (frosting, public client, groups `[helter]`). After this change its client gets `[GoogleSAML]`, the same as the console edit, so the reconcile no longer reverts it. cupcake-backend is `client: none` and is unaffected. - **IdPs:** the internal pool has exactly one IdP, `GoogleSAML`, so the configured list resolves. The external pool keeps `["COGNITO"]`, which is what it gets today (it has no IdPs). - **Plan expectation:** - in-place updates: `aws_cognito_user_pool.internal` (`admin_create_user_config` is not ForceNew), `aws_lambda_function.auth_claims` (env) and `aws_lambda_function.app_auth_sync` (env + code), `aws_sns_topic_policy.alerts` (new statement); - creates: the log metric filter and the alarm (the log group already exists); - no destroys, no pool replacement. ## Rollout & user actions - None needed after merge. The CI apply updates the pool and both Lambdas. - The next auth-request change, or the 03:30 UTC reconcile, sets every internal client to `[GoogleSAML]`. To apply it right away, run `aws lambda invoke --function-name javabin-app-auth-sync --payload '{"action":"reconcile"}' --cli-binary-format raw-in-base64-out /dev/stdout`. - Existing sessions keep working. Refresh tokens pass the domain check because all current users qualify. ## Follow-ups - **Interaction with #158 (`feat/alb-login`).** That PR edits the same functions in app-auth-sync (`client_settings`, `sync`, `reconcile`) and adds `auth_claims_external` from the same handler. The external function doesn't set `REQUIRED_EMAIL_DOMAIN`, so it skips the domain check, which is intended. - Rebase on top of this PR: `client_settings()` now takes `pool` first. - `service-routing`'s hardcoded `internal_identity_provider = "GoogleSAML"` can be dropped once clients have a single IdP. The hosted UI already redirects. - **Overlap with #165.** It adds `deletion_protection = "ACTIVE"` to both pools (raised in review here too; left to #165 to avoid a duplicate). The change is a different attribute, so expect a trivial text merge at most. - **Not done here:** `user_attribute_update_settings { attributes_require_verification_before_update = ["email"] }` on the internal pool. Blocking the user-admin scope closes the reported path; the pool setting would also affect how SAML attribute mapping updates email, which can't be checked without applying, so it is left for a separate, tested change. - **#157 next step: switching to Google OIDC.** - Set `internal_identity_providers = ["Google"]` once the IdP exists. - Google users then count as federated. That is safe only with an Internal-consent GCP client (java.no accounts only), as the design note proposes. Otherwise, map `email_verified` and drop Google from the trusted list. - Bring `GoogleSAML` under Terraform with an `import` block, or remove it after cutover. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Alexanderamiri
added a commit
that referenced
this pull request
Oct 24, 2026
…th doctor (#167) ## Why Part of #157. Internal-pool sign-in goes through the `GoogleSAML` app, which always answers for the browser's active Google account. A personal Gmail gets a dead-end `403 app_not_configured_for_user` page with no account chooser. The classic hosted UI drops `prompt=`, so Cognito can't ask SAML for one either. The fix is a Google OIDC IdP, with an Internal-consent OAuth client and `prompt=select_account`. The investigation also found that teams can't see why sign-in fails, and can't recover: - invalid requests and console drift are visible only in CloudWatch; - a replaced client leaves running tasks on a deleted client ID; - callback typos show up only as `redirect_mismatch` in the browser; - an `auth.groups` typo denies everyone. **Merge #166 (internal-pool lockdown) first.** This branch is rebased on it and reuses its per-pool IdP list, so until #166 merges this PR's diff and plan also contain #166's two commits. After #166 lands, GitHub shows only the last commit. This PR is safe to merge before the Google credentials exist. Every Google behaviour change is gated, and with #166 the internal default is `["GoogleSAML"]` (what cupcake's client has live today). ## What **identity (Terraform)** - `aws_cognito_identity_provider.internal_google` is now gated on SSM `/javabin/platform/google-oauth/internal/{client-id,client-secret}`, read with `aws_ssm_parameters_by_path`, instead of a tfvar. Its count stays 0 until both exist. - `authorize_scopes = "openid email profile"`, and mapping `email`, `email_verified`, `name`, `username=sub`. - `authorize_url = …/v2/auth?hd=java.no&prompt=select_account`. You can turn this off with `google_authorize_url_params = false` for the managed-login fallback. - Uses #166's per-pool lists `internal_identity_providers` (default `["GoogleSAML"]`, COGNITO refused) and `external_identity_providers` (`["COGNITO"]`), env `INTERNAL_IDENTITY_PROVIDERS`/`EXTERNAL_IDENTITY_PROVIDERS`. The same internal list is auth-claims' `FEDERATED_IDENTITY_PROVIDERS`, so the cutover changes clients and the trust list together. - Adding the Google IdP changes no client until this list names it. - An output precondition fails the plan if `Google` is listed without the SSM credentials or with an empty `google_workspace_domain` (auth-claims' `REQUIRED_EMAIL_DOMAIN`). - auth-claims: IdPs in `EMAIL_VERIFIED_IDENTITY_PROVIDERS` (default `Google`) are not trusted on the IdP name alone; their users also need `email_verified=true`, mapped from Google's claim. - The external pool keeps its `google_client_id` tfvar path. It is unchanged and still empty. **app-auth-sync** - `cognito-client-id`, `-secret` and the new `-identity-provider` are written **only when the value changes**. - The secret is not decrypted. It changes only with a new client ID. - The client ID is written last, so restarted tasks find the matching secret. - Writes `/javabin/apps/{team}/{app}/auth-status` (String JSON, always valid and under 4096 bytes: list fields are dropped, never sliced). Fields: `status` (synced/invalid/error), `error`, `client_id`, `client_id_changed_at` (carried over between syncs), `identity_providers`, `pool_domain`, URLs, `drift_corrected` (fields a reconcile reverted; also logged as `WARN drift`), `unknown_groups`/`empty_groups` (checked against `group#` items in the memberships table), `request_version`, `synced_at`. - A declared IdP missing from the pool is #166's `PoolMisconfigured` skip: status `error` in auth-status, the client is left as it is. - Reconcile no longer stops at a failing app, and returns normally instead of raising (an async invoke would retry the whole run twice). The failure is logged as `Sync failed for …`, which #166's `javabin-app-auth-sync-skipped` metric filter now also counts. - Callback/logout URLs with a `#fragment` are rejected by the Lambda too. - IAM: `dynamodb:GetItem` on the memberships table, plus write access to `*/cognito-client-identity-provider` and `*/auth-status`. **Team access to auth-status** - Teams read it through their existing `/javabin/apps/{team}/*` read grant. - Writes are denied for `auth-status` in the team-provisioner permission-set policy (CI-applied), and in `terraform/org/identity-center.tf` (human-applied; see user actions). **app-secret-rollout** - `cognito-client-id` and `cognito-client-identity-provider` are no longer ignored. A replaced client (case 9/15) or an IdP cutover now restarts the service. - `auth-status` and `cognito-client-secret` are ignored: auth-status is rewritten on every sync, and a new secret always comes with a new ID written right after it, so restarting on the secret would deploy once with the old ID. - There is no restart loop, because unchanged values are never rewritten. **generate-modules.py** - Callback/logout URLs must: - use `https`, except `http://localhost` / `127.0.0.1`; - have no `#fragment`; - have a host equal to `routing.host` or listed in the new optional `auth.extra_hosts`. - `client: none` combined with `callback_urls`, `logout_urls`, `scopes` or `extra_hosts` is rejected; these were silently ignored before. - `COGNITO_IDENTITY_PROVIDER` is injected (an ECS secret from `module.auth.identity_provider_arn`) and can be aliased through `auth.env`. - Auth resolution is factored into `resolve_auth()`, which the doctor reuses. - Both live apps (frosting, cupcake-backend) still generate. Tests use their verbatim app.yaml. **cognito-app-client module** - New outputs `identity_provider_arn` and `auth_status_parameter_name`. **scripts/auth-doctor.py** - Uses team credentials only: app.yaml, the app's own SSM parameters, and unauthenticated HTTP. - Checks: - the request matches app.yaml; - auth-status is synced for the current request version, and its client ID matches `cognito-client-id`; - drift and unknown/empty groups are reported; - `/oauth2/authorize` for every callback URL redirects to the IdP, not `/error?error=redirect_mismatch` (it warns if Google isn't given `prompt=select_account`); - `/logout` for every logout URL redirects back (a mismatch lands on the hosted UI `/login`, verified live); - best effort, WARN only: the service's PRIMARY-deployment tasks started after `client_id_changed_at` (skipped until auth-status records one; `cognito-client-id`'s LastModifiedDate is useless because the old code rewrote it on every reconcile). - A non-JSON auth-status is a FAIL, not a traceback. When auth-status is missing altogether, the doctor waits at most 30 s instead of the full `--wait`. - `--json` is the hook for a future `javabin auth doctor`. javabin-cli is not changed here. - `tf-apply.yml` gets a non-blocking `Auth doctor` step (`continue-on-error`, `--wait 180`, skipped for apps without auth). **platform-ci** - After apply, `scripts/reconcile-app-auth.sh` queues an async reconcile. This writes the new `cognito-client-identity-provider` and `auth-status` parameters right away, so an app redeploy that picks up the new `COGNITO_IDENTITY_PROVIDER` secret doesn't hit `ParameterNotFound`. It is non-blocking, and the 03:30 reconcile is the backstop. **Docs** - `docs/cognito-google-setup.md` is rewritten: - exact GCP steps (Internal consent screen, a separate Web OAuth client, redirect `https://javabin-internal.auth.eu-central-1.amazoncognito.com/oauth2/idpresponse`; the old doc listed a nonexistent `auth-internal.javazone.no`); - SSM commands; - the post-apply curl probe and the managed-login fallback; - cutover and user plan. - `docs/cognito-saml-setup.md` is marked superseded. It also noted that its groups mapping never existed. - `docs/app-yaml-reference.md`: IdP list, URL rules and `extra_hosts`, the `/logout` exact-match rule, group denial (`error_description`), `COGNITO_IDENTITY_PROVIDER`, auth-status and the doctor. - `docs/lambda-functions.md`, `docs/reusable-modules.md` and `CLAUDE.md` are updated. ## How verified - `python3 -m unittest discover -s terraform/lambda-src/tests` passes (111, including #166's), and `-s scripts/tests` passes (67, including the fmt check of generated TF). New tests cover: - no-op syncs write only auth-status; - secret written before ID on replacement; - status content: invalid, error, drift vs request change, unknown/empty groups; - declared IdP list, cutover switching the IdP param without touching the client ID, a missing IdP failing cleanly; - reconcile continuing past a failing app; - rollout restarting on `cognito-client-*`; - URL validation; - doctor scenarios, including non-JSON status, the short wait for a missing status, and draining/stale tasks; - auth-status size limit (bytes, valid JSON), `client_id_changed_at` carry-over, fragments rejected; - auth-claims refusing Google users without `email_verified=true`. - `terraform fmt -recursive -check` and `terraform validate` (platform root) pass. - Live, read-only: the doctor against frosting's app.yaml. - Request check passes. auth-status doesn't exist yet, as expected before deploy. - Probes against the live `cupcake-cupcake` client: callback → GoogleSAML ✔, `/nope` → `redirect_mismatch` ✔, logout exact ✔, logout without trailing slash → hosted UI `/login` (this is why the logout check looks for "redirects back"). - Live pool state checked: 4 users, all `GoogleSAML_*` external-provider users, no native users. `/javabin/apps/*` has only cupcake's `auth` and `cognito-client-id`. ## Rollout & user actions Merging (after #166) changes nothing about Google sign-in: - The Google IdP isn't created without SSM credentials. - The internal IdP list default is #166's `["GoogleSAML"]`, which equals cupcake's live client (`SupportedIdentityProviders=[GoogleSAML]`). The post-apply reconcile therefore does not put COGNITO back; on main without #166, the old 03:30 reconcile would. - On the first reconcile, cupcake gets `cognito-client-identity-provider = GoogleSAML` (Create, so no restart) and an `auth-status`. Expected plan: the `aws_ssm_parameters_by_path` read, and in-place updates to the app-auth-sync role policy, function env/code, app-secret-rollout code and team-provisioner code. No destroys. User actions: 1. **Human apply `terraform/org`** (identity-center.tf). It adds `auth-status` to the `javabin-developer` permission set's platform-managed deny. Per-team permission sets get it from team-provisioner on its next run. 2. **GCP (when ready):** create the Internal-consent OAuth client and store it in SSM, following `docs/cognito-google-setup.md` §1–2. Then re-run platform CI on main. The plan creates the `Google` IdP, and clients are untouched. 3. **Verify:** run the §3 curl probe (or the doctor after cutover) to confirm Cognito keeps `prompt=select_account`. If it doesn't, use the documented managed-login fallback. 4. **Cutover (separate PR):** `internal_identity_providers = ["Google"]`. #166 (server-side domain check, no self sign-up) is a hard prerequisite, which stacking on it guarantees. - Users get a new `sub`. Platform memberships and groups are keyed by email, so nothing to migrate there; cupcake should check for `sub`-keyed data. - Delete the 4 `GoogleSAML_*` users and the `GoogleSAML` IdP a week later, and turn off the Google Admin SAML app. ## Interactions - **#166 (lockdown):** this PR is stacked on it and reuses its variable, output, env names and `identity_providers(pool, upid)`. It adds to #166's code: the Google precondition on `internal_identity_providers`, `EMAIL_VERIFIED_IDENTITY_PROVIDERS` in auth-claims, and a second pattern on the skipped-sync metric filter. If #166 is squash-merged, I rebase this branch onto main with `--onto`. - **#158 / `feat/alb-login`:** both change `app_auth_sync` (`client_settings`, `sync`, `_put_param`, `reconcile`) and `generate-modules.py` auth handling. Expect textual conflicts; the slot threading composes with this. - Login slots should write `auth-status` and `cognito-client-identity-provider` next to the slot request (`…/login-N/`). The rollout regex already ignores slot params. - service-routing's hardcoded `internal_identity_provider = "GoogleSAML"` should go away: with a single IdP per client it is unnecessary, and after cutover it points at a deleted IdP. Otherwise read the slot's `cognito-client-identity-provider`. - **#156:** also edits `app_secret_rollout/handler.py`, a small conflict. ## Follow-ups - The developer boundary (team CI roles, gate-vended apply credentials) doesn't deny writing `auth-status`/`cognito-client-*`. It is at 5,946/6,144 bytes and #156 adds to it, so I left it alone; the limitation is now documented in `docs/lambda-functions.md` and `docs/app-yaml-reference.md`. The impact stays inside the team's own app: a forged status, or one extra restart before the next sync puts a credential back. A Deny in the team CI role policies is the follow-up if we want it enforced. - Drop `ALLOW_USER_SRP_AUTH` from internal clients. With #166 there is no COGNITO and no self sign-up, so it is unused there, but changing client auth flows is #166-adjacent scope. - `javabin auth doctor` in javabin-cli, wrapping `scripts/auth-doctor.py --json`. - Doctor check that the task definition contains every `auth.env` alias; canary-client probe in platform CI after identity applies. - `auth.accept_clients` for `client: none` backends (case 13). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
App developers should be able to put simple apps and static pages behind a Cognito login with a few lines of app.yaml, and choose per path which groups may see them. This must work for the internal pool (Google Workspace SAML) and the external pool (attendees and other public users). Until now the only option was
auth:, where the app has to do OIDC itself.What
New
routing.loginin app.yaml:Shorthands:
login: internal/login: external, or a single mapping.Generator (
scripts/generate-modules.py): validates the new field (forms, 1-10 rules, 1-4 paths per rule, ALB path characters, no duplicate paths, nothing after a/*rule,publiccannot be combined withpool/groups, room belowrouting.priority, group names). It emitslogin_rules(p1..pN) at prioritiespriority-1,priority-2, ... and warns when no rule covers/*, because path patterns are case-sensitive. Apps withoutlogingenerate exactly the same Terraform as before.service-routing: for each protected rule:
{team}-{app}.login-N(nestedcognito-app-client, slotlogin-N);time_sleepwhile app-auth-sync creates it (re-armed only when the request changes);authenticate-oidc+ forward listener rule on host and paths. The internal pool goes straight toGoogleSAML. Sessions last 8 h.Public rules only forward. The host catch-all rule
depends_onthe login rules, so a new app fails closed.cognito-app-client: new optional
slotinput. New outputsclient_id_parameter_name,client_secret_parameter_nameandrequest_value. The external pool falls back to its prefix domain, which also fixes the emptyCOGNITO_DOMAINforauth: external.app-auth-sync: accepts
/javabin/apps/{team}/{app}/login-N/auth. It names the client{team}-{app}.login-N;.cannot occur in app names, so the name cannot collide with another app's client. Credentials go next to the request, and reconcile handles slots. IAM and the EventBridge pattern already match the deeper paths, because their*covers/.identity: the external pool gets:
javabin-externalprefix domain (only while no custom domain is configured);javabin-auth-claims-external: same zip and role,REGISTRY_GROUPS=false.The external pool had no domain and no trigger before. Anyone can sign up there with any email, so registry groups are never granted there; native groups and per-client
allowed_groupsstill apply.Docs:
app-yaml-reference.md(newrouting.loginsection: what the app receives inx-amzn-oidc-*headers, caveats, and a static-pages recipe using nginx-unprivileged),reusable-modules.md,lambda-functions.md,CLAUDE.md.Why
authenticate-oidcand notauthenticate-cognito: the Cognito action needscognito-idp:DescribeUserPoolClienton the platform pools. The developer boundary denies that, and allowing it would let any team read every team's client secret. Group checks reuse the existing auth-claimsallowed_groupsrefusal. That check works per client, which is why there is one client per rule. Cognito's session means a user still signs in only once.Design:
explore/auth-proxy-design.md(outside the repo).Review fixes (second commit)
admin_create_user_config { allow_admin_create_user_only = true }stops self sign-up. app-auth-sync no longer addsCOGNITOto internal-pool clients, only the pool's IdPs (GoogleSAML). That matches the livecupcake-cupcakeclient, which already has onlyGoogleSAML. Before this, alogin: internalrule would have let anyone register with a password through the hosted UI.attributes_require_verification_before_update = ["email"]. auth-claims grants registry groups only to federated users (cognito:user_status = EXTERNAL_PROVIDER) or users withemail_verified = true. Live SAML users haveemail_verified=false, which is why the federated check is needed.javabin-auth-p<N>-<hash of slot, pool, groups>. Tightening groups or reordering rules now forces a fresh sign-in, so an older session from a looser rule no longer works./x/*now also covers/x. The warning and the docs now also cover percent-encoding and...logindoes not proxy to sites hosted off the platform.Decision for you to confirm
"Reverse proxy" here means a sign-in gate in front of apps running on the platform (any ECS app, or an nginx container for static pages). The ALB cannot forward to upstream URLs outside the platform. Proxying to external sites would need a small proxy container or CloudFront, and is not in this PR.
How verified
python3 -m unittest discover scripts/tests(50) andterraform/lambda-src/tests(81): all pass. New tests cover:login,terraform fmtclean;REGISTRY_GROUPS=false; unverified native emails get no registry groups;COGNITO;terraform fmt -recursive -checkpasses.terraform validatepasses formodules/service-routing,modules/cognito-app-clientand a generated sample app (internal + external + public rules, module sources pointed at this branch).terraform planof that sample app against the account (local state, nothing applied):/javabin/apps/core/wiki/login-N/auth);javabin-internalwithidentity_provider=GoogleSAML; the external rule points atjavabin-external.auth...and the external pool's issuer;lambda_config,auth_claims_external+ permission and the prefix domain;Follow-ups
COGNITOprovider already close sign-up.fix/cognito-internal-lockdownthat does a broader version of the internal-pool lockdown (per-pool IdP list,@java.nodomain check). Whichever PR merges second needs a rebase inapp_auth_sync/handler.py,auth_claims/handler.pyandidentity/main.tf.DenyXTeam.PriorityInUse. The docs tell developers to leave room. A registry check that reserves[priority-10, priority]per app is the proper fix (registry follow-up). Today cupcake-backend (101) cannot add rules because cupcake holds 100.🤖 Generated with Claude Code