Skip to content

End-user auth at the edge

The second deep page in the trust arc. Every cloud ships a platform-run OAuth proxy in front of your service that authenticates the end user, then injects the verified identity into a request header your backend reads. Enable it once at the edge; your app stops reinventing OIDC.

The three surfaces

Cloud Edge auth product Headers on the request to your backend
Azure App Service / Container Apps Easy Auth X-MS-CLIENT-PRINCIPAL (Base64 JSON) + X-MS-CLIENT-PRINCIPAL-ID (Entra oid) + X-MS-CLIENT-PRINCIPAL-NAME + X-MS-CLIENT-PRINCIPAL-IDP
GCP Identity-Aware Proxy (IAP) x-goog-iap-jwt-assertion — signed JWT with sub + email
AWS Application Load Balancer OIDC x-amzn-oidc-identity (sub, plaintext) + x-amzn-oidc-accesstoken + x-amzn-oidc-data (ES256-signed JWT with sub + email)

The load-bearing lesson: the trust boundary

Every one of these proxies works the same way: it strips client-supplied copies of these headers on inbound requests, then re-injects them from its own signed data. That's why your backend can trust them — but only if the proxy is on every path to your service.

If your service is also reachable via a VPC-internal ingress that skips the proxy, or via a NodePort someone forgot to firewall, or via a Cloud Run direct URL that isn't behind IAP — a caller on the internal path can send those same header names raw, and your app can't tell the difference. The proxy's stripping happens at the proxy; nothing inside your app enforces it.

The deep rule that follows from that:

Wherever you can't guarantee 100% of traffic flows through the proxy, verify the JWT yourself — signature, audience, expiry, and (for AWS) the ALB-ARN signer field. Details on Verify the JWT.

Concretely:

  • Azure App Service — Easy Auth is in-process (IIS module on Windows; ambassador container on Linux). No internal bypass path exists for the same App Service; you can trust the injected headers there. The moment you put App Service behind Azure Front Door or Application Gateway and also expose the origin at all, that guarantee is gone.
  • GCP IAP — protects an HTTP(S) Load Balancer's backend service. If the backend is also reachable via internal VPC network from workloads outside IAP, verify the JWT.
  • AWS ALB OIDC — signs the JWT with an ES256 key per region; AWS ships no proxy that strips the header at the target level. If your target is reachable outside the ALB (a peered VPC, another SG, a bastion), a caller can send x-amzn-oidc-data raw. Always verify the signature and check the signer claim matches your ALB's ARN.

Enabling config per cloud

Portal path: App Service → Authentication → Add identity provider → Microsoft. Choose "Require authentication" so unauthenticated requests get redirected to Entra sign-in (HTTP 302 for browsers, HTTP 401 for APIs).

Terraform (azurerm_linux_web_app or azurerm_container_app) exposes the config via auth_settings_v2:

resource "azurerm_linux_web_app" "chiron" {
  name                = "${var.name}-app"
  resource_group_name = module.baseline.resource_group_name
  location            = var.region
  service_plan_id     = azurerm_service_plan.chiron.id
  site_config         {}

  auth_settings_v2 {
    auth_enabled           = true
    require_authentication = true
    default_provider       = "azureactivedirectory"
    unauthenticated_action = "RedirectToLoginPage"

    active_directory_v2 {
      client_id                  = azuread_application.chiron.client_id
      tenant_auth_endpoint       = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0"
      allowed_audiences          = [azuread_application.chiron.client_id]
      # client_secret_setting_name = "AAD_CLIENT_SECRET"   # if using client secret
    }

    login {
      token_store_enabled = true
    }
  }
}

The Base64-encoded claim blob and simple oid/name/idp headers show up per App Service — Work with user identities. Full JSON claims are also fetchable at /.auth/me; sign-out at /.auth/logout.

Console path: Security → Identity-Aware Proxy → toggle IAP ON for the backend service. Configure the OAuth consent screen and IAP-enabled OAuth client (auto-generated on first enable), then grant users roles/iap.httpsResourceAccessor on the resource:

gcloud iap web add-iam-policy-binding \
  --resource-type=backend-services \
  --service=chiron-backend \
  --member=user:alice@example.com \
  --role=roles/iap.httpsResourceAccessor

Terraform (google_iap_web_backend_service_iam_binding on a Compute Engine backend service, or google_iap_settings for a wider config):

resource "google_iap_web_backend_service_iam_binding" "chiron" {
  project             = var.project_id
  web_backend_service = google_compute_backend_service.chiron.name
  role                = "roles/iap.httpsResourceAccessor"
  members             = ["group:eng@example.com"]
}

Every backend request lands with x-goog-iap-jwt-assertion — a signed JWT whose sub + email are the verified user. Full spec: cloud.google.com/iap/docs/signed-headers-howto.

Attach an authenticate-oidc action to a listener rule on your Application Load Balancer. It runs before the forward action, so unauthenticated requests are bounced to your IdP's /authorize endpoint. Terraform:

resource "aws_lb_listener_rule" "chiron_auth" {
  listener_arn = aws_lb_listener.chiron.arn
  priority     = 100

  action {
    type = "authenticate-oidc"
    authenticate_oidc {
      issuer                 = "https://idp.example.com"
      authorization_endpoint = "https://idp.example.com/oauth2/authorize"
      token_endpoint         = "https://idp.example.com/oauth2/token"
      user_info_endpoint     = "https://idp.example.com/oauth2/userinfo"
      client_id              = var.oidc_client_id
      client_secret          = var.oidc_client_secret     # store in Secrets Manager, wire in via data source
      session_cookie_name    = "chironAuth"
      session_timeout        = 3600
      scope                  = "openid email"
      on_unauthenticated_request = "authenticate"
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.chiron.arn
  }

  condition {
    path_pattern { values = ["/*"] }
  }
}

Every authenticated request lands with x-amzn-oidc-data (ES256-signed JWT) plus the plaintext x-amzn-oidc-identity. Full spec: docs.aws.amazon.com/…/listener-authenticate-users.

What the app reads

The surface intro has the concise "read this header, verify, done" story. This page cares about what "verify" means when your topology has more than one path to the workload. The next page walks the exact verify_token call per cloud.

Docs verified 2026-08-08

Where the arc goes next

  • Verify the JWT — the exact per-cloud signature/audience/signer verification code, plus a defense-in-depth edge_claim module you can drop into any Chiron recipe.