Skip to content

feat(identity): cut the internal pool over from GoogleSAML to Google - #172

Closed
Alexanderamiri wants to merge 3 commits into
feat/cognito-managed-login-v2from
feat/cognito-google-cutover
Closed

Alexanderamiri wants to merge 3 commits into
feat/cognito-managed-login-v2from
feat/cognito-google-cutover

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

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). With the Google OIDC provider on managed login, the prompt=select_account that apps and the ALB send (#170) reaches Google. Stacked on #171. DRAFT: do not merge until the checklist below is done.

What

  • internal_identity_providers default ["GoogleSAML"] → ["Google"]. One list feeds every internal app client (app-auth-sync) and auth-claims' FEDERATED_IDENTITY_PROVIDERS.
  • Docs updated to describe the post-cutover state.

How verified

  • terraform fmt -check -recursive terraform
  • Expected 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).

Rollout: pre-cutover checklist

  • feat(identity): managed-login branding per app client and prompt=select_account #170 is applied and every internal client has a branding style (loop in docs/cognito-google-setup.md step 3)
  • feat(identity): switch the internal pool domain to managed login #171 is applied (describe-user-pool-domain --domain javabin-internal → ManagedLoginVersion: 2), and cupcake sign-in still works on GoogleSAML
  • Internal-pool lockdown (fix(identity): lock down the internal Cognito pool #166) is live: REQUIRED_EMAIL_DOMAIN, no self sign-up, no COGNITO on internal clients
  • /javabin/platform/google-oauth/internal/{client-id,client-secret} exist (the Google IdP is in the pool)
  • frosting sends prompt: 'select_account' (javaBin/frosting PR), and any other SPA/backend sends COGNITO_EXTRA_AUTH_PARAMS
  • 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
  • Probe, with a test client that lists Google:
    DOMAIN=javabin-internal.auth.eu-central-1.amazoncognito.com
    curl -s -o /dev/null -w '%{redirect_url}\n' \
      "https://$DOMAIN/oauth2/authorize?response_type=code&client_id=<id>&redirect_uri=<callback>&scope=openid&identity_provider=Google&prompt=select_account" \
      | grep -q 'prompt=select_account' && echo OK || echo "prompt dropped"
    If it prints prompt dropped even on managed login, the Google-type provider does not get prompt: switch to an OIDC-type provider before cutting over

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.
  2. Sign in once with a personal Gmail 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

Alexanderamiri and others added 3 commits October 29, 2026 22:45
…ct_account

app-auth-sync gives every client it manages (including login-N slot clients) a
managed-login branding style with Cognito's defaults, only when it has none,
and deletes it with the client; reconcile backfills existing clients. The
role may create/describe/delete styles on the two pools only.

Prepares the internal pool domain for managed login, which forwards
prompt=select_account to Google (the classic hosted UI ignores it):
- internal_managed_login_version (default 1, unchanged) gates the switch
- the Google provider's authorize_url is plain: Cognito dropped the query
  string, which left a perpetual plan diff
- ALB sign-in rules on the internal pool send prompt=select_account
- apps get COGNITO_EXTRA_AUTH_PARAMS=prompt=select_account (internal pool)
- auth-doctor sends the same parameters in its authorize probe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Managed login forwards prompt=select_account to Google, so heroes get the
account chooser. Every app client has a branding style from app-auth-sync
(previous change), which managed login needs to show a sign-in page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
internal_identity_providers = ["Google"]: every internal app client and
auth-claims' FEDERATED_IDENTITY_PROVIDERS move to the Google OIDC provider,
which shows Google's account chooser on managed login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
Acquiring state lock. This may take a few moments...
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:
  ~ update in-place

Terraform will perform the following actions:

  # module.identity.aws_cognito_user_pool_domain.internal will be updated in-place
  ~ resource "aws_cognito_user_pool_domain" "internal" {
        id                              = "javabin-internal"
      ~ managed_login_version           = 1 -> 2
        # (8 unchanged attributes hidden)
    }

  # module.identity.aws_lambda_function.auth_claims will be updated in-place
  ~ resource "aws_lambda_function" "auth_claims" {
        id                             = "javabin-auth-claims"
        tags                           = {}
        # (23 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              ~ "FEDERATED_IDENTITY_PROVIDERS" = "GoogleSAML" -> "Google"
                # (2 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

  # module.lambdas.aws_iam_role_policy.app_auth_sync will be updated in-place
  ~ resource "aws_iam_role_policy" "app_auth_sync" {
        id     = "javabin-app-auth-sync:javabin-app-auth-sync"
        name   = "javabin-app-auth-sync"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action   = [
                            "cognito-idp:CreateUserPoolClient",
                            "cognito-idp:UpdateUserPoolClient",
                            "cognito-idp:DeleteUserPoolClient",
                            "cognito-idp:DescribeUserPoolClient",
                            "cognito-idp:ListUserPoolClients",
                            "cognito-idp:ListIdentityProviders",
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD",
                            "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_gdFOsE4EM",
                        ]
                        Sid      = "ManageAppClients"
                    },
                  + {
                      + Action   = [
                          + "cognito-idp:CreateManagedLoginBranding",
                          + "cognito-idp:DescribeManagedLoginBrandingByClient",
                          + "cognito-idp:DeleteManagedLoginBranding",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_Icikv3dtD",
                          + "arn:aws:cognito-idp:eu-central-1:553637109631:userpool/eu-central-1_gdFOsE4EM",
                        ]
                      + Sid      = "ManageAppClientBranding"
                    },
                    {
                        Action   = "cognito-idp:ListUserPools"
                        Effect   = "Allow"
                        Resource = "*"
                        Sid      = "FindPools"
                    },
                    # (3 unchanged elements hidden)
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # 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-26T14:01:56.000+0000" -> (known after apply)
      ~ source_code_hash               = "GvhzSxolQGY8f4/fMAxElLFmKIXk8607SS3v89Em3x4=" -> "HKStd1VAWlTHRUFr+guun1HqV1RHui7nc1qw1cRONzY="
        tags                           = {}
        # (21 unchanged attributes hidden)

      ~ environment {
          ~ variables = {
              ~ "INTERNAL_IDENTITY_PROVIDERS" = "GoogleSAML" -> "Google"
                # (5 unchanged elements hidden)
            }
        }

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 4 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: 🟡 MEDIUM

Plan updates 4 existing resources in place (0 add, 0 destroy): a Cognito domain managed-login version bump, a Lambda identity-provider env var change, an IAM policy gaining managed-login branding permissions, and a Lambda code/env update.

  • 🔒 [security] The app_auth_sync IAM role policy gains a new Allow statement (ManageAppClientBranding) granting cognito-idp:CreateManagedLoginBranding, DescribeManagedLoginBrandingByClient and DeleteManagedLoginBranding on both user pools in eu-central-1 (553637109631). This widens the Lambda's permissions, so it is worth confirming it is needed for the managed login v2 change.
  • 🔒 [security] The Cognito domain 'javabin-internal' moves managed_login_version from 1 to 2, changing the hosted login experience for internal users. Verify that the new login UI and branding are intended and that the internal users' sign-in flow is tested.
  • ✅ [routine] Federated/internal identity provider configuration changes: FEDERATED_IDENTITY_PROVIDERS on auth_claims goes from 'GoogleSAML' to 'Google', and INTERNAL_IDENTITY_PROVIDERS on app_auth_sync goes from 'GoogleSAML' to 'Google'. This removes SAML as an identity provider in the auth flow, so users who depend on SAML login could be locked out. Confirm this is intentional and that SAML is no longer needed.
  • ✅ [routine] app_auth_sync Lambda code is updated in place (source_code_hash changes). No replacement or destruction; routine deployment of new code.
  • 💰 [cost] No new billable resources are created (0 to add). No NAT gateways, load balancers, or databases are involved, so cost impact is negligible.

@Alexanderamiri
Alexanderamiri force-pushed the feat/cognito-managed-login-v2 branch from 89e780b to cb6a0f4 Compare October 29, 2026 22:05
@Alexanderamiri
Alexanderamiri deleted the branch feat/cognito-managed-login-v2 October 29, 2026 22:06
Alexanderamiri added a commit that referenced this pull request Oct 30, 2026
…#174)

## 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](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