Securing your app with signed headers

This page describes how to secure your app with signed IAPheaders. When configured, Identity-Aware Proxy (IAP) uses JSONWeb Tokens (JWT) to make sure that a request to your app isauthorized. This protects your app from the following risks:

  • IAP is accidentally disabled
  • Misconfigured firewalls
  • Unauthorized access from within the project

To help secure your app, you must use signed headers for all app types.

Alternatively, if you have an App Engine standard environment app, youcan use theUsers API.

Compute Engine and GKE health checksdon't include JWT headers and IAP doesn't process healthchecks. If your health check returns access errors, make sure that you have the health check configured correctly in the Google Cloud console and thatyour JWT header validation allows the health check path. For moreinformation, seeCreate a health check exception.

Before you begin

To secure your app with signed headers, you'll need the following:

Securing your app with IAP headers

To secure your app with the IAP JWT, verify the header,payload, and signature of the JWT. The JWT is in the HTTP request headerx-goog-iap-jwt-assertion. If an attacker bypasses IAP, theattacker can forge the IAP unsigned identity headers,x-goog-authenticated-user-{email,id}. The IAP JWT providesa more secure alternative.

Signed headers provide secondary security in case someone bypassesIAP. When IAP is enabled, IAPstrips thex-goog-* headers provided by the client when the request goesthrough the IAP serving infrastructure.

Verifying the JWT header

Verify that the JWT's header conforms to the following constraints:

JWT Header Claims
algAlgorithmES256
kidKey ID Must correspond to one of the public keys listed in the IAP key file, available in two different formats:https://www.gstatic.com/iap/verify/public_key andhttps://www.gstatic.com/iap/verify/public_key-jwk

Ensure that the JWT was signed by the private key that corresponds tothe token'skid claim. First, retrieve the public key from one of two places:

  • https://www.gstatic.com/iap/verify/public_key. This URL contains a JSONdictionary that maps thekid claims to the public key values.
  • https://www.gstatic.com/iap/verify/public_key-jwk. This URL containsthe IAP public keys inJWK format.

After you have the public key, use a JWT library to verify the signature.

IAP periodically rotates its public keys. To make sure that you can always verify the JWTs, seeAutomate public key caching.

Verifying the JWT payload

Verify the JWT's payload conforms to the following constraints:

JWT Payload Claims
expExpiration time Must be in the future. The time is measured in seconds since the UNIX epoch. Allow 30 seconds for skew.The maximum lifetime of a token is 10 minutes + 2 * skew.
iatIssued-at time Must be in the past. The time is measured in seconds since the UNIX epoch. Allow 30 seconds for skew.
audAudience Must be a string with the following values:
  • App Engine:/projects/PROJECT_NUMBER/apps/PROJECT_ID
  • Compute Engine and GKE:/projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID
  • Cloud Run:/projects/PROJECT_NUMBER/locations/REGION/services/SERVICE_NAME
issIssuer Must behttps://cloud.google.com/iap.
hdAccount domain If an account belongs to a hosted domain, thehd claim is provided to differentiate the domain the account is associated with.
googleGoogle claim If one or moreaccess levels apply to the request, their names are stored within thegoogle claim's JSON object, under theaccess_levels key, as an array of strings.

When you specify a device policy and the Org has access to the device data, theDeviceId is also stored in the JSON object. Note that a request going to another Org might not have permission to view the device data.

You can get the values for theaud string mentioned above by accessing theGoogle Cloud console, or you can use the gcloud command-line tool.

To getaud string values from the Google Cloud console, go to theIdentity-Aware Proxy settingsfor your project, clickMore next to the Load Balancer resource, and thenselectSigned Header JWT Audience. TheSigned Header JWT dialog thatappears shows theaud claim for the selected resource.

If you want to use thegcloud CLIgcloud command-line tool to get theaud string values, you'll need to knowthe project ID. You can find the project ID on theGoogle Cloud consoleProject info card, then run the specified commands for each value.

Project number

To get your project number using the gcloud command-line tool, run the following command:

gcloud projects describePROJECT_ID

The command returns output like the following:

createTime:'2016-10-13T16:44:28.170Z'lifecycleState:ACTIVEname:project_nameparent:id:'433637338589'type:organizationprojectId:PROJECT_IDprojectNumber:'PROJECT_NUMBER'

Service ID

To get your service ID using the gcloud command-line tool, run the following command:

gcloud compute backend-services describeSERVICE_NAME --project=PROJECT_ID --global

The command returns output like the following:

affinityCookieTtlSec:0backends:-balancingMode:UTILIZATIONcapacityScaler:1.0group:https://www.googleapis.com/compute/v1/projects/project_name/regions/us-central1/instanceGroups/my-groupconnectionDraining:drainingTimeoutSec:0creationTimestamp:'2017-04-03T14:01:35.687-07:00'description:''enableCDN:falsefingerprint:zaOnO4k56Cw=healthChecks:-https://www.googleapis.com/compute/v1/projects/project_name/global/httpsHealthChecks/my-hcid:'SERVICE_ID'kind:compute#backendServiceloadBalancingScheme:EXTERNALname:my-serviceport:8443portName:httpsprotocol:HTTPSselfLink:https://www.googleapis.com/compute/v1/projects/project_name/global/backendServices/my-servicesessionAffinity:NONEtimeoutSec:3610

Retrieving the user identity

If all of the preceding verifications are successful, retrieve the user identity.The ID token's payload contains the following user information:

ID Token Payload User Identity
subSubject The unique, stable identifier for the user. Use this value instead of thex-goog-authenticated-user-id header.
emailUser emailUser email address.
  • Use this value instead of thex-goog-authenticated-user-email header.
  • Unlike that header and thesub claim, this value doesn't have a namespace prefix.

Following is sample code to secure an app with signed IAPheaders:

C#

usingGoogle.Apis.Auth;usingGoogle.Apis.Auth.OAuth2;usingSystem;usingSystem.Threading;usingSystem.Threading.Tasks;publicclassIAPTokenVerification{/// <summary>/// Verifies a signed jwt token and returns its payload./// </summary>/// <param name="signedJwt">The token to verify.</param>/// <param name="expectedAudience">The audience that the token should be meant for./// Validation will fail if that's not the case.</param>/// <param name="cancellationToken">The cancellation token to propagate cancellation requests.</param>/// <returns>A task that when completed will have as its result the payload of the verified token.</returns>/// <exception cref="InvalidJwtException">If verification failed. The message of the exception will contain/// information as to why the token failed.</exception>publicasyncTask<JsonWebSignature.Payload>VerifyTokenAsync(stringsignedJwt,stringexpectedAudience,CancellationTokencancellationToken=default){SignedTokenVerificationOptionsoptions=newSignedTokenVerificationOptions{// Use clock tolerance to account for possible clock differences// between the issuer and the verifier.IssuedAtClockTolerance=TimeSpan.FromMinutes(1),ExpiryClockTolerance=TimeSpan.FromMinutes(1),TrustedAudiences={expectedAudience},TrustedIssuers={"https://cloud.google.com/iap"},CertificatesUrl=GoogleAuthConsts.IapKeySetUrl,};returnawaitJsonWebSignature.VerifySignedTokenAsync(signedJwt,options,cancellationToken:cancellationToken);}}

Go

import("context""fmt""io""google.golang.org/api/idtoken")// validateJWTFromAppEngine validates a JWT found in the// "x-goog-iap-jwt-assertion" header.funcvalidateJWTFromAppEngine(wio.Writer,iapJWT,projectNumber,projectIDstring)error{// iapJWT := "YmFzZQ==.ZW5jb2RlZA==.and0" // req.Header.Get("X-Goog-IAP-JWT-Assertion")// projectNumber := "123456789"// projectID := "your-project-id"ctx:=context.Background()aud:=fmt.Sprintf("/projects/%s/apps/%s",projectNumber,projectID)payload,err:=idtoken.Validate(ctx,iapJWT,aud)iferr!=nil{returnfmt.Errorf("idtoken.Validate: %w",err)}// payload contains the JWT claims for further inspection or validationfmt.Fprintf(w,"payload: %v",payload)returnnil}// validateJWTFromComputeEngine validates a JWT found in the// "x-goog-iap-jwt-assertion" header.funcvalidateJWTFromComputeEngine(wio.Writer,iapJWT,projectNumber,backendServiceIDstring)error{// iapJWT := "YmFzZQ==.ZW5jb2RlZA==.and0" // req.Header.Get("X-Goog-IAP-JWT-Assertion")// projectNumber := "123456789"// backendServiceID := "backend-service-id"ctx:=context.Background()aud:=fmt.Sprintf("/projects/%s/global/backendServices/%s",projectNumber,backendServiceID)payload,err:=idtoken.Validate(ctx,iapJWT,aud)iferr!=nil{returnfmt.Errorf("idtoken.Validate: %w",err)}// payload contains the JWT claims for further inspection or validationfmt.Fprintf(w,"payload: %v",payload)returnnil}

Java

importcom.google.api.client.http.HttpRequest;importcom.google.api.client.json.webtoken.JsonWebToken;importcom.google.auth.oauth2.TokenVerifier;/** Verify IAP authorization JWT token in incoming request. */publicclassVerifyIapRequestHeader{privatestaticfinalStringIAP_ISSUER_URL="https://cloud.google.com/iap";// Verify jwt tokens addressed to IAP protected resources on App Engine.// The project *number* for your Google Cloud project via 'gcloud projects describe $PROJECT_ID'// The project *number* can also be retrieved from the Project Info card in Cloud Console.// projectId is The project *ID* for your Google Cloud Project.booleanverifyJwtForAppEngine(HttpRequestrequest,longprojectNumber,StringprojectId)throwsException{// Check for iap jwt header in incoming requestStringjwt=request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");if(jwt==null){returnfalse;}returnverifyJwt(jwt,String.format("/projects/%s/apps/%s",Long.toUnsignedString(projectNumber),projectId));}booleanverifyJwtForComputeEngine(HttpRequestrequest,longprojectNumber,longbackendServiceId)throwsException{// Check for iap jwt header in incoming requestStringjwtToken=request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");if(jwtToken==null){returnfalse;}returnverifyJwt(jwtToken,String.format("/projects/%s/global/backendServices/%s",Long.toUnsignedString(projectNumber),Long.toUnsignedString(backendServiceId)));}privatebooleanverifyJwt(StringjwtToken,StringexpectedAudience){TokenVerifiertokenVerifier=TokenVerifier.newBuilder().setAudience(expectedAudience).setIssuer(IAP_ISSUER_URL).build();try{JsonWebTokenjsonWebToken=tokenVerifier.verify(jwtToken);// Verify that the token contain subject and email claimsJsonWebToken.Payloadpayload=jsonWebToken.getPayload();returnpayload.getSubject()!=null &&payload.get("email")!=null;}catch(TokenVerifier.VerificationExceptione){System.out.println(e.getMessage());returnfalse;}}}

Node.js

/** * TODO(developer): Uncomment these variables before running the sample. */// const iapJwt = 'SOME_ID_TOKEN'; // JWT from the "x-goog-iap-jwt-assertion" headerletexpectedAudience=null;if(projectNumber &&projectId){// Expected Audience for App Engine.expectedAudience=`/projects/${projectNumber}/apps/${projectId}`;}elseif(projectNumber &&backendServiceId){// Expected Audience for Compute EngineexpectedAudience=`/projects/${projectNumber}/global/backendServices/${backendServiceId}`;}constoAuth2Client=newOAuth2Client();asyncfunctionverify(){// Verify the id_token, and access the claims.constresponse=awaitoAuth2Client.getIapPublicKeys();constticket=awaitoAuth2Client.verifySignedJwtWithCertsAsync(iapJwt,response.pubkeys,expectedAudience,['https://cloud.google.com/iap'],);// Print out the info contained in the IAP ID tokenconsole.log(ticket);}verify().catch(console.error);

PHP

namespace Google\Cloud\Samples\Iap;# Imports Google auth libraries for IAP validationuse Google\Auth\AccessToken;/** * Validate a JWT passed to your App Engine app by Identity-Aware Proxy. * * @param string $iapJwt The contents of the X-Goog-IAP-JWT-Assertion header. * @param string $cloudProjectNumber The project *number* for your Google *     Cloud project. This is returned by 'gcloud projects describe $PROJECT_ID', *     or in the Project Info card in Cloud Console. * @param string $cloudProjectId Your Google Cloud Project ID. */function validate_jwt_from_app_engine(    string $iapJwt,    string $cloudProjectNumber,    string $cloudProjectId): void {    $expectedAudience = sprintf(        '/projects/%s/apps/%s',        $cloudProjectNumber,        $cloudProjectId    );    validate_jwt($iapJwt, $expectedAudience);}/** * Validate a JWT passed to your Compute / Container Engine app by Identity-Aware Proxy. * * @param string $iapJwt The contents of the X-Goog-IAP-JWT-Assertion header. * @param string $cloudProjectNumber The project *number* for your Google *     Cloud project. This is returned by 'gcloud projects describe $PROJECT_ID', *     or in the Project Info card in Cloud Console. * @param string $backendServiceId The ID of the backend service used to access the *     application. See https://cloud.google.com/iap/docs/signed-headers-howto *     for details on how to get this value. */function validate_jwt_from_compute_engine(    string $iapJwt,    string $cloudProjectNumber,    string $backendServiceId): void {    $expectedAudience = sprintf(        '/projects/%s/global/backendServices/%s',        $cloudProjectNumber,        $backendServiceId    );    validate_jwt($iapJwt, $expectedAudience);}/** * Validate a JWT passed to your app by Identity-Aware Proxy. * * @param string $iapJwt The contents of the X-Goog-IAP-JWT-Assertion header. * @param string $expectedAudience The expected audience of the JWT with the following formats: *     App Engine:     /projects/{PROJECT_NUMBER}/apps/{PROJECT_ID} *     Compute Engine: /projects/{PROJECT_NUMBER}/global/backendServices/{BACKEND_SERVICE_ID} */function validate_jwt(string $iapJwt, string $expectedAudience): void{    // Validate the signature using the IAP cert URL.    $token = new AccessToken();    $jwt = $token->verify($iapJwt, [        'certsLocation' => AccessToken::IAP_CERT_URL    ]);    if (!$jwt) {        print('Failed to validate JWT: Invalid JWT');        return;    }    // Validate token by checking issuer and audience fields.    assert($jwt['iss'] == 'https://cloud.google.com/iap');    assert($jwt['aud'] == $expectedAudience);    print('Printing user identity information from ID token payload:');    printf('sub: %s', $jwt['sub']);    printf('email: %s', $jwt['email']);}

Python

fromgoogle.auth.transportimportrequestsfromgoogle.oauth2importid_tokendefvalidate_iap_jwt(iap_jwt,expected_audience):"""Validate an IAP JWT.    Args:      iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.      expected_audience: The Signed Header JWT audience. See          https://cloud.google.com/iap/docs/signed-headers-howto          for details on how to get this value.    Returns:      (user_id, user_email, error_str).    """try:decoded_jwt=id_token.verify_token(iap_jwt,requests.Request(),audience=expected_audience,certs_url="https://www.gstatic.com/iap/verify/public_key",)return(decoded_jwt["sub"],decoded_jwt["email"],"")exceptExceptionase:return(None,None,f"**ERROR: JWT validation error{e}**")

Ruby

# iap_jwt = "The contents of the X-Goog-Iap-Jwt-Assertion header"# project_number = "The project *number* for your Google Cloud project"# project_id = "Your Google Cloud project ID"# backend_service_id = "Your Compute Engine backend service ID"require"googleauth"audience=nilifproject_number &&project_id# Expected audience for App Engineaudience="/projects/#{project_number}/apps/#{project_id}"elsifproject_number &&backend_service_id# Expected audience for Compute Engineaudience="/projects/#{project_number}/global/backendServices/#{backend_service_id}"end# The client ID as the target audience for IAPpayload=Google::Auth::IDTokens.verify_iapiap_jwt,aud:audienceputspayloadifaudience.nil?puts"Audience not verified! Supply a project_number and project_id to verify"end

Testing your validation code

If you visit your app using thesecure_token_test query parameters,IAP will include an invalid JWT. Use this to make sure yourJWT-validation logic is handling all of the various failure cases and to seehow your app behaves when it receives an invalid JWT.

Creating a health check exception

As mentioned previously, Compute Engine and GKEhealth checks don't use JWT headers and IAP doesn't handlehealth checks. You'll need to configure yourhealth check and app to allow thehealth check access.

Configuring the health check

If you haven't already set a path for your health check, use theGoogle Cloud console to set a non-sensitive path for the health check. Makesure this path isn't shared by any other resource.

  1. Go to the Google Cloud consoleHealth checks page.
    Go to the Health checks page
  2. Click the health check you're using for your app, then clickEdit.
  3. UnderRequest path add a non-sensitive path name. This specifies the URL path that Google Cloud uses when sending health check requests. If omitted, the health check request is sent to/.
  4. ClickSave.

Configuring the JWT validation

In your code that calls the JWT validation routine, add a condition to serve an200 HTTP status for your health check request path. For example:

if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH'  return HttpResponse(status=200)elseVALIDATION_FUNCTION

Automate public key caching

IAP rotates its public keys periodically. To ensure you canalways verify the IAP JWT, we recommend that youcache the keys to avoid fetching them from the public URL for each requestand that you automate the process of updating the cached key. This approachis particularly useful for applications that run in an environment with network restrictions, like a VPC Service Controls perimeter.

A VPC Service Controls perimeter can prevent direct access to the public URLfor the keys. By caching the keys in a Cloud Storage bucket, yourapplications can fetch them from a location within your VPC-SC perimeter.

The following Terraform configuration deploys a function toCloud Run that fetches the latest IAP public keys fromhttps://www.gstatic.com/iap/verify/public_key-jwk and stores them in aCloud Storage bucket. A Cloud Scheduler job triggers this function every12 hours to keep the keys up to date.

This setup includes the following:

  • Necessary Google Cloud APIs enabled to use Cloud Run and store and cache keys
  • A Cloud Storage bucket to store the fetched IAP public keys
  • A Cloud Storage bucket to stage Cloud Run functions source code
  • Service accounts for Cloud Run functions and Cloud Scheduler with appropriate IAM permissions
  • A Python function to fetch and store keys
  • A Cloud Scheduler job to trigger the function every 12 hours

Directory structure

├── function_source/│   ├── main.py│   └── requirements.txt├── main.tf├── outputs.tf├── variables.tf└── terraform.tfvars

function_source/main.py

importfunctions_frameworkimportrequestsfromgoogle.cloudimportstorageimportos# Environment variables to be set in the function configurationBUCKET_NAME=os.environ.get("BUCKET_NAME")OBJECT_NAME=os.environ.get("OBJECT_NAME","iap_public_keys.jwk")IAP_KEYS_URL="https://www.gstatic.com/iap/verify/public_key-jwk"@functions_framework.httpdefupdate_iap_keys(request):"""Fetches IAP public keys from the public URL and stores them in a Cloud Storage bucket."""ifnotBUCKET_NAME:print("Error: BUCKET_NAME environment variable not set.")return"BUCKET_NAME environment variable not set.",500try:# Fetch the keysresponse=requests.get(IAP_KEYS_URL)response.raise_for_status()# Raise an exception for bad status codeskeys_content=response.textprint(f"Successfully fetched keys from{IAP_KEYS_URL}")# Store in Cloud Storagestorage_client=storage.Client()bucket=storage_client.bucket(BUCKET_NAME)blob=bucket.blob(OBJECT_NAME)blob.upload_from_string(keys_content,content_type='application/json')print(f"Successfully wrote IAP keys to gs://{BUCKET_NAME}/{OBJECT_NAME}")returnf"Successfully updated{OBJECT_NAME} in bucket{BUCKET_NAME}",200exceptrequests.exceptions.RequestExceptionase:print(f"Error fetching keys from{IAP_KEYS_URL}:{e}")returnf"Error fetching keys:{e}",500exceptExceptionase:print(f"Error interacting with Cloud Storage:{e}")returnf"Error interacting with Cloud Storage:{e}",500

Replace the following:

  • BUCKET_NAME: the name of your Cloud Storage bucket
  • OBJECT_NAME: the name of the object to store your keys to

function_source/requirements.txt

functions-framework==3.*requestsgoogle-cloud-storage

variables.tf

variable"project_id"{description="The Google Cloud project ID."type=stringdefault=PROJECT_ID}variable"region"{description="The Google Cloud region."type=stringdefault="REGION"}variable"iap_keys_bucket_name"{description="The name of the Cloud Storage bucket to store IAP keys."type=stringdefault=BUCKET_NAME"}variable"function_source_bucket_name"{description="The name of the Cloud Storage bucket to store the function source code."type=stringdefault="BUCKET_NAME_FUNCTION"}

Replace the following:

  • PROJECT_ID: your Google Cloud project ID
  • REGION: the region to deploy resources in—for example,us-central1
  • BUCKET_NAME: the name for the Cloud Storage bucket that stores IAP keys
  • BUCKET_NAME_FUNCTION: the name for the Cloud Storage bucket that stores Cloud Run functions source code

main.tf

terraform{required_providers{google={source="hashicorp/google"version=">= 4.50.0"}google-beta={source="hashicorp/google-beta"version=">= 4.50.0"}}}provider"google"{project=var.project_idregion=var.region}provider"google-beta"{project=var.project_idregion=var.region}# Enable necessary APIsresource"google_project_service""services"{for_each=toset(["storage.googleapis.com","cloudfunctions.googleapis.com","run.googleapis.com", # Cloud Functions v2 uses Cloud Run"cloudscheduler.googleapis.com","iamcredentials.googleapis.com","cloudbuild.googleapis.com" # Needed for Cloud Functions deployment])service=each.keydisable_on_destroy=false}# Cloud Storage Bucket to store the IAP public keysresource"google_storage_bucket""iap_keys_bucket"{name=var.iap_keys_bucket_namelocation=var.regionuniform_bucket_level_access=trueversioning{enabled=true}lifecycle{prevent_destroy=false # Set to true in production to prevent accidental deletion}}# Cloud Storage Bucket to store the Cloud Function source coderesource"google_storage_bucket""function_source_bucket"{name=var.function_source_bucket_namelocation=var.regionuniform_bucket_level_access=true}# Archive the function source codedata"archive_file""function_source_zip"{type="zip"source_dir="${path.module}/function_source"output_path="${path.module}/function_source.zip"}# Upload the zipped source code to the source bucketresource"google_storage_bucket_object""function_source_object"{name="function_source.zip"bucket=google_storage_bucket.function_source_bucket.namesource=data.archive_file.function_source_zip.output_path}# Service Account for the Cloud Functionresource"google_service_account""iap_key_updater_sa"{account_id="iap-key-updater"display_name="IAP Key Updater Function SA"}# Grant the function's SA permission to write to the IAP keys bucketresource"google_storage_bucket_iam_member""keys_bucket_writer"{bucket=google_storage_bucket.iap_keys_bucket.namerole="roles/storage.objectAdmin"member="serviceAccount:${google_service_account.iap_key_updater_sa.email}"}# Cloud Function (v2)resource"google_cloudfunctions2_function""update_iap_keys_func"{provider=google-beta # CFv2 often has newer features in google-betaname="update-iap-keys-function"location=var.regionbuild_config{runtime="python312"entry_point="update_iap_keys"source{storage_source{bucket=google_storage_bucket.function_source_bucket.nameobject=google_storage_bucket_object.function_source_object.name}}}service_config{max_instance_count=1available_memory="256M"timeout_seconds=60ingress_settings="ALLOW_ALL"service_account_email=google_service_account.iap_key_updater_sa.emailenvironment_variables={BUCKET_NAME=google_storage_bucket.iap_keys_bucket.nameOBJECT_NAME="iap_public_keys.jwk"}}depends_on=[google_project_service.services,google_storage_bucket_iam_member.keys_bucket_writer]}# Service Account for the Cloud Scheduler jobresource"google_service_account""iap_key_scheduler_sa"{account_id="iap-key-scheduler"display_name="IAP Key Update Scheduler SA"}# Grant the Scheduler SA permission to invoke the Cloud Functionresource"google_cloudfunctions2_function_iam_member""invoker"{provider=google-betaproject=google_cloudfunctions2_function.update_iap_keys_func.projectlocation=google_cloudfunctions2_function.update_iap_keys_func.locationcloud_function=google_cloudfunctions2_function.update_iap_keys_func.namerole="roles/cloudfunctions.invoker"member="serviceAccount:${google_service_account.iap_key_scheduler_sa.email}"}# Cloud Scheduler Jobresource"google_cloud_scheduler_job""iap_key_update_schedule"{name="iap-key-update-schedule"description="Fetches IAP public keys and stores them in Cloud Storage every 12 hours"schedule="0 */12 * * *" # Every 12 hourstime_zone="Etc/UTC"region=var.regionhttp_target{uri=google_cloudfunctions2_function.update_iap_keys_func.service_config[0].urihttp_method="POST"oidc_token{service_account_email=google_service_account.iap_key_scheduler_sa.email}}depends_on=[google_cloudfunctions2_function_iam_member.invoker,google_project_service.services]}

outputs.tf

output"iap_keys_bucket_url"{description="The Cloud Storage bucket URL where IAP public keys are stored."value="gs://${google_storage_bucket.iap_keys_bucket.name}"}output"cloud_function_url"{description="The URL of the Cloud Function endpoint that triggers key updates."value=google_cloudfunctions2_function.update_iap_keys_func.service_config[0].uri}

terraform.tfvars

Create aterraform.tfvars file to specify your project ID and customizebucket names if needed:

project_id="your-gcp-project-id"# Optional: Customize bucket names# iap_keys_bucket_name = "custom-iap-keys-bucket"# function_source_bucket_name = "custom-func-src-bucket"

Deploy with Terraform

  1. Save the files in the directory structure described previously.
  2. Navigate to the directory in your terminal and initialize Terraform:
    terraforminit
  3. Plan the changes:
    terraformplan
  4. Apply the changes:
    terraformapply

This deploys the infrastructure. The Cloud Scheduler job triggers thefunction every 12 hours, fetching the IAP keys and storing themings://BUCKET_NAME/iap_public_keys.jwk by default. Yourapplications can now fetch the keys from this bucket.

Clean up resources

To remove the resources created by Terraform, run the following commands:

gsutilrm-ags://BUCKET_NAME/**terraformdestroy-auto-approve

ReplaceBUCKET_NAME with the Cloud Storage bucket for your keys.

Note: Thegsutil command is necessary because Terraform doesn't destroyCloud Storage buckets that contain objects when versioning is enabled.

JWTs for external identities

If you're using IAP with external identities,IAP will still issue a signed JWT on every authenticatedrequest, just as it does with Google identities. However, there are a fewdifferences.

Provider information

When using external identities, the JWT payload will contain a claimnamedgcip. This claim contains user information, such as their email, photoURL, and any additional provider-specific attributes.

The following is an example of a JWT for a user who logged in with Facebook:

"gcip":'{  "auth_time": 1553219869,  "email": "facebook_user@gmail.com",  "email_verified": false,  "firebase": {    "identities": {      "email": [        "facebook_user@gmail.com"      ],      "facebook.com": [        "1234567890"      ]    },    "sign_in_provider": "facebook.com",  },  "name": "Facebook User",  "picture: "https://graph.facebook.com/1234567890/picture",  "sub": "gZG0yELPypZElTmAT9I55prjHg63"}',

Theemail andsub fields

If a user was authenticated by Identity Platform, theemail andsubfields of the JWT will be prefixed with the Identity Platform token issuerand the tenant ID used (if any). For example:

"email":"securetoken.google.com/PROJECT-ID/TENANT-ID:demo_user@gmail.com","sub":"securetoken.google.com/PROJECT-ID/TENANT-ID:gZG0yELPypZElTmAT9I55prjHg63"

Controlling access withsign_in_attributes

IAM doesn't support external identities, but you can use claimsembedded in thesign_in_attributes field to control access. For example,consider a user signed in using a SAML provider:

{"aud":"/projects/project_number/apps/my_project_id","gcip":'{    "auth_time": 1553219869,    "email": "demo_user@gmail.com",    "email_verified": true,    "firebase": {      "identities": {        "email": [          "demo_user@gmail.com"        ],        "saml.myProvider": [          "demo_user@gmail.com"        ]      },      "sign_in_attributes": {        "firstname": "John",        "group": "test group",        "role": "admin",        "lastname": "Doe"      },      "sign_in_provider": "saml.myProvider",      "tenant": "my_tenant_id"    },    "sub": "gZG0yELPypZElTmAT9I55prjHg63"  }',"email":"securetoken.google.com/my_project_id/my_tenant_id:demo_user@gmail.com","exp":1553220470,"iat":1553219870,"iss":"https://cloud.google.com/iap","sub":"securetoken.google.com/my_project_id/my_tenant_id:gZG0yELPypZElTmAT9I55prjHg63"}

You could add logic to your application similar to the code below to restrictaccess to users with a valid role:

constgcipClaims=JSON.parse(decodedIapJwtClaims.gcip);if(gcipClaims&&gcipClaims.firebase&&gcipClaims.firebase.sign_in_attributes&&gcipClaims.firebase.sign_in_attribute.role==='admin'){//Allowaccesstoadminrestrictedresource.}else{//Blockaccess.}

You can access additional user attributes from Identity Platform SAML andOIDC providers using thegcipClaims.gcip.firebase.sign_in_attributes nestedclaim.

IdP claims size limitations

After a user signs in with Identity Platform, the additional user attributeswill be propagated to the stateless Identity Platform ID token payload, whichwill be securely passed to IAP. IAP willthen issue its own stateless opaque cookie, which also contains the same claims.IAP will generate the signed JWT header based on the cookiecontent.

As a result, if a session is initiated with many claims, it mightexceed the maximum allowed cookie size, which is typically about 4KB in most browsers.This will cause the sign-in operation to fail.

Make sure that only the necessary claims are propagated in the IdP SAMLor OIDC attributes. Another option is to useblocking functions to filter outthe claims that aren't required for the authorization check.

const gcipCloudFunctions = require('gcip-cloud-functions');const authFunctions = new gcipCloudFunctions.Auth().functions();// This function runs before any sign-in operation.exports.beforeSignIn = authFunctions.beforeSignInHandler((user, context) => {  if (context.credential &&      context.credential.providerId === 'saml.my-provider') {    // Get the original claims.    const claims = context.credential.claims;    // Define this function to filter out the unnecessary claims.    claims.groups = keepNeededClaims(claims.groups);    // Return only the needed claims. The claims will be propagated to the token    // payload.    return {      sessionClaims: claims,    };  }});

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2026-02-18 UTC.