Skip to content

feat(identity): platform-owned IdP probe client for the internal pool - #174

Merged
Alexanderamiri merged 1 commit into
mainfrom
feat/idp-probe-client
Oct 30, 2026
Merged

Alexanderamiri merged 1 commit into
mainfrom
feat/idp-probe-client

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

Why

Before cutting the internal pool over from GoogleSAML to the Google IdP (#172) we need to know whether managed login forwards prompt=select_account from /oauth2/authorize to Google. No client lists Google, so the redirect cannot be probed today.

What

  • app-auth-sync: requests owned by team platform (/javabin/apps/platform/...; platform is excluded from registered teams, so only the platform's own Terraform can write there) may set identity_providers to override the pool's IdP list. Each IdP must exist in the pool (else status error, client untouched). The internal pool never allows COGNITO. Any other team's request with the field is invalid.
  • identity module: aws_ssm_parameter.idp_probe_auth writes /javabin/apps/platform/idp-probe/auth. app-auth-sync turns it into client platform-idp-probe with a managed-login branding style, like any app client. The client lists var.internal_idp_probe_identity_providers (default ["Google"]). It is created only once the Google IdP exists, and [] removes it.
  • The probe can't be used to sign in: it is a public client with no secret, code flow, callback https://localhost/idp-probe, scopes openid email, and allowed group idp-probe-nobody, which does not exist. auth-claims refuses every token, and the domain check applies as well.
  • Docs: the probe command is in docs/cognito-google-setup.md step 3, and the override is described in docs/lambda-functions.md.

How verified

  • python3 -m unittest discover -s terraform/lambda-src/tests: 219 OK. New tests cover the platform override with a branding style, app clients keeping the pool list, other teams being refused, an IdP missing from the pool, and COGNITO/empty/malformed lists.
  • python3 -m unittest discover -s scripts/tests: OK.
  • terraform fmt -check -recursive terraform and terraform validate: OK.

Rollout

Merge, then apply. The plan should only create the SSM parameter and update the app-auth-sync Lambda code, with no Cognito pool, domain or client replacement. The Create event, plus the post-apply reconcile, creates the client and its style. Then run the probe from the docs. No app client changes.

🤖 Generated with Claude Code

Adds platform-idp-probe, an internal-pool client that lists only the IdPs in
internal_idp_probe_identity_providers (default Google), so the redirect
managed login sends to Google can be checked before the cutover. The
identity module writes it as an app-auth-sync request under
/javabin/apps/platform/; app-auth-sync lets only platform requests override
the pool's IdP list (each IdP must exist; never COGNITO in the internal
pool). Public client, localhost callback, and a nonexistent allowed group,
so it can never issue a token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 30, 2026 08:28
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 1 to add, 1 to change, 0 to destroy.

Plan output
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.identity.aws_ssm_parameter.idp_probe_auth[0] will be created
  + resource "aws_ssm_parameter" "idp_probe_auth" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + description    = "IdP probe client request for app-auth-sync (platform-owned)"
      + has_value_wo   = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/javabin/apps/platform/idp-probe/auth"
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

  # module.lambdas.aws_lambda_function.app_auth_sync will be updated in-place
  ~ resource "aws_lambda_function" "app_auth_sync" {
        id                             = "javabin-app-auth-sync"
      ~ last_modified                  = "2026-10-29T22:04:09.000+0000" -> (known after apply)
      ~ source_code_hash               = "HKStd1VAWlTHRUFr+guun1HqV1RHui7nc1qw1cRONzY=" -> "/APYypBRHgw0Sth8xhiuImapO9oNdBwMtRPosf3/9Rg="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 1 to add, 1 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Plan creates one SSM parameter for a platform-owned IdP probe credential and updates the app-auth-sync Lambda's code in place (source hash change only); nothing is destroyed.

  • 🔒 [security] New SSM parameter /javabin/apps/platform/idp-probe/auth is created with a sensitive value, type String (not SecureString). The value is marked sensitive in plan output, but the parameter type should be confirmed: a credential-like value stored as String is readable by anyone with ssm:GetParameter on that path. Consider switching to SecureString with a KMS key.
  • 🔒 [security] The parameter holds an IdP probe client request credential that the Lambda may consume. Verify the Lambda's IAM role is scoped to read only this path and that no broader ssm:GetParameter wildcard was granted; IAM changes are not shown in this plan, so this is a check, not a confirmed issue.
  • ✅ [routine] Lambda javabin-app-auth-sync is updated in place: only source_code_hash changes (new deployment package). No configuration, role, or networking changes are shown. Standard deploy-time change.
  • 💰 [cost] One standard SSM parameter (no meaningful cost) and an in-place Lambda code update. No new billable resources such as NAT gateways, load balancers, or databases.
  • 💥 [destruction] 0 resources destroyed. The Lambda is updated in place, not replaced.

@Alexanderamiri
Alexanderamiri merged commit 71ab7aa into main Oct 30, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the feat/idp-probe-client branch October 30, 2026 08:30
Alexanderamiri added a commit that referenced this pull request Oct 30, 2026
… prompt (#176)

## Why
The probe client (#174) showed that Cognito managed login forwards
`prompt=select_account` to Google **only when the request also sends
`identity_provider`**. `auth-doctor.py` always adds `identity_provider`,
so an app that sends only `prompt` passed but would get no account
chooser after the Google cutover (#175).

## What
- New `chooser` check (WARN only): an app that renames auth variables
via `auth.env` but doesn't alias `COGNITO_IDENTITY_PROVIDER` almost
certainly doesn't send it. Apps that read the platform variables
directly aren't flagged, since the doctor can't see their code.
- The authorize warning and the docstring explain the
`identity_provider` requirement.

## How verified
- `python3 -m unittest discover -s scripts/tests`: 133 tests, OK. There
are 4 new tests: aliased without IdP warns, aliased with IdP passes, no
aliases passes, and the probe names the IdP.
- `python3 -m unittest discover -s terraform/lambda-src/tests`: 219
tests, OK.
- Live against the current pool:
  - frosting's `app.yaml` on main gets `WARN chooser`.
- The `app.yaml` from javaBin/frosting#258, which aliases
`COGNITO_IDENTITY_PROVIDER`, passes with no warning.

## Rollout
Script only, with no Terraform changes. App deploys pick it up from
platform `main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Alexanderamiri added a commit that referenced this pull request Oct 30, 2026
…175)

Replaces #172 (auto-closed when its base branch was deleted). Rebased
onto main: #170, #173 (managed login) and #174 (IdP probe client) are
merged and applied.

## Why

GoogleSAML has no account chooser. A browser signed in to a personal
Google account hits a dead-end `403 app_not_configured_for_user`
(#157). The Google provider on managed login can show
Google's account chooser, but only if `prompt=select_account` reaches
Google. **DRAFT: do not merge until the checklist below is done.**

## Probe result (2026-10-30, `platform-idp-probe`, client
`75ejr2cdi133ictvip1mc32nr3`, lists only `Google`)

Each request got a 302 straight to
`accounts.google.com/o/oauth2/v2/auth`. Results were the same on
repeated runs:

| `/oauth2/authorize` extra params | `prompt` in Google URL | `hd` in
Google URL |
|---|---|---|
| `prompt=select_account` | **absent** | absent |
| `prompt=select_account&identity_provider=Google` |
`prompt=select_account` | absent |
| `identity_provider=Google` | absent | absent |
| none | absent | absent |
| `prompt=select_account&identity_provider=Google&hd=java.no` |
`prompt=select_account` | absent |

Managed login forwards `prompt` only when the request names the IdP. On
the single-IdP auto-redirect it drops it. `hd` is never forwarded.
Domain enforcement relies on the Internal consent screen and
auth-claims, as designed. The generic OIDC IdP fallback is **not
needed**: the Google-type IdP does get `prompt`, the request just has to
include `identity_provider`. So this PR adds that fix instead of the
fallback.

## What

- `internal_identity_providers` default changes from `["GoogleSAML"]` to
`["Google"]`. One list feeds every internal app client (app-auth-sync)
and auth-claims' `FEDERATED_IDENTITY_PROVIDERS`.
- **ALB sign-in** (`terraform/modules/service-routing`): internal-pool
rules now send `identity_provider=<client's IdP>` next to
`prompt=select_account`. The IdP is read from the client's
`cognito-client-identity-provider` parameter, which app-auth-sync
writes, so it is never hardcoded and follows the cutover. Adds a
`identity_provider_parameter_name` output to `cognito-app-client`.
- **Apps:** `docs/app-yaml-reference.md` "Account chooser" now says to
send `identity_provider=$COGNITO_IDENTITY_PROVIDER` together with
`COGNITO_EXTRA_AUTH_PARAMS`. The generate-modules comment and
`docs/cognito-google-setup.md` step 3 record the probe result.

## How verified

- `python3 -m unittest discover -s scripts/tests`: OK. `python3 -m
unittest discover -s terraform/lambda-src/tests`: 219 OK.
- `terraform fmt -check -recursive terraform` and `terraform validate`
on `service-routing`.
- Expected platform plan: the app-auth-sync and auth-claims Lambda
environments are updated in place. No Cognito pool, domain or client is
replaced (Terraform manages no clients). After apply, the CI reconcile
switches every internal client to `Google` and rewrites
`cognito-client-identity-provider`, which restarts each auth app
(app-secret-rollout).
- The service-routing change reaches app repos on their next deploy.
Before the cutover it sends `identity_provider=GoogleSAML`, which
matches today's auto-redirect.

## Rollout: pre-cutover checklist

- [x] #170 applied, every internal client has a branding style
(app-auth-sync)
- [x] #173 applied: the internal domain is on managed login
- [ ] Internal-pool lockdown (#166) is live: `REQUIRED_EMAIL_DOMAIN`, no
self sign-up, no `COGNITO` on internal clients
- [x]
`/javabin/platform/google-oauth/internal/{client-id,client-secret}`
exist (the `Google` IdP is in the pool)
- [x] Probe passes when `identity_provider` is sent (table above)
- [ ] Apps that use ALB sign-in redeploy with this service-routing
version
- [ ] frosting and any other SPA/backend send `prompt=select_account`
**and** `identity_provider=$COGNITO_IDENTITY_PROVIDER`. `prompt` alone
is dropped
- [ ] Apps keyed by `sub` (check cupcake/frosting) can handle a new
`sub` per user. Google users are `Google_<sub>` with a new `sub`. The
platform keys on email

## Rollout: after merge

1. `python3 scripts/auth-doctor.py --app-yaml <app>/app.yaml` for each
auth app. Expect a redirect to `accounts.google.com` with
`prompt=select_account`. auth-doctor sends `identity_provider` like a
correct app, so also check that the app itself sends it.
2. Sign in once with a personal Gmail account active in the browser.
Google must show the account chooser.
3. A week later, delete the 4 `GoogleSAML_*` users, the `GoogleSAML`
provider and the Google Admin SAML app (`docs/cognito-google-setup.md`,
"Decommission").

Rollback: revert to `["GoogleSAML"]` (GoogleSAML still exists until
decommission).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant