Securing your app with signed headers

This page describes how to secure your app with signed Cloud IAP headers. When configured, Cloud Identity-Aware Proxy (Cloud IAP) uses JSON Web Tokens (JWT) to make sure that a request to your app is authorized. This protects your app from the following kind of risks:

  • IAP is accidentally disabled;
  • Misconfigured firewalls;
  • Access from within the project.

To properly secure your app, you must use signed headers for App Engine flexible environment, Compute Engine, and Kubernetes Engine applications. Signed headers aren't supported for App Engine standard environment applications. Instead, those applications should use the approach described in getting the User's Identity.

Note that Compute Engine and Kubernetes Engine health checks don't include JWT headers and Cloud IAP doesn't handle health checks. If your health check returns access errors, make sure that you have it configured correctly in the Cloud Platform Console and that your JWT header validation whitelists the health check path. For more information, see Create a health check exception.

Before you begin

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

Securing your app with Cloud IAP headers

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

Signed headers provide secondary security in case someone bypasses Cloud IAP. Note that when Cloud IAP is turned on, it strips the x-goog-* headers provided by the client when the request goes through the Cloud IAP serving infrastructure.

Verify the ID token header

Verify that the ID token's header conforms to the following constraints:

ID Token Header Claims
alg Algorithm ES256
kid Key ID Must correspond to one of the public keys listed in the Cloud IAP key file, available in two different formats: https://www.gstatic.com/iap/verify/public_key and https://www.gstatic.com/iap/verify/public_key-jwk

Make sure that the ID token was signed by the private key that corresponds to the token's kid claim. Grab the public key from one of two places:

  • https://www.gstatic.com/iap/verify/public_key. This URL contains a JSON dictionary that maps the kid claims to the public key values.
  • https://www.gstatic.com/iap/verify/public_key-jwk. This URL contains the Cloud IAP public keys in JWK format.

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

Verify the ID token payload

Verify the ID token's payload conforms to the following constraints:

ID Token Payload Claims
exp Expiration time Must be in the future. The time is measured in seconds since the UNIX epoch.
iat Issued-at time Must be in the past. The time is measured in seconds since the UNIX epoch.
aud Audience Must be a string with the following values:
  • App Engine: /projects/PROJECT_NUMBER/apps/PROJECT_ID
  • Compute Engine and Kubernetes Engine: /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID
iss Issuer Must be https://cloud.google.com/iap.

You can get the values for the aud string mentioned above by accessing the Cloud Platform Console, or you can use the gcloud command-line tool.

To get aud string values from the Cloud Platform Console, go to the Identity-Aware Proxy settings for your project, click More next to the Load Balancer resource, and then select Signed Header JWT Audience. The Signed Header JWT dialog that appears displays the aud claim for the selected resource.

overflow menu with the Signed Header JWT Audience option

If you want to use the Cloud SDK gcloud command-line tool to get the aud string values, you'll need to know the project ID. You can find the project ID on the Cloud Platform Console Project info card, then run the specified commands below for each value.

Project number

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

gcloud projects describe PROJECT_ID

The command returns output like the following:

createTime: '2016-10-13T16:44:28.170Z'
lifecycleState: ACTIVE
name: project_name
parent:
  id: '433637338589'
  type: organization
projectId: PROJECT_ID
projectNumber: 'PROJECT_NUMBER'

Service ID

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

gcloud compute backend-services describe SERVICE_NAME --project=PROJECT_ID --global

The command returns output like the following:

affinityCookieTtlSec: 0
backends:
- balancingMode: UTILIZATION
  capacityScaler: 1.0
  group: https://www.googleapis.com/compute/v1/projects/project_name/regions/us-central1/instanceGroups/my-group
connectionDraining:
  drainingTimeoutSec: 0
creationTimestamp: '2017-04-03T14:01:35.687-07:00'
description: ''
enableCDN: false
fingerprint: zaOnO4k56Cw=
healthChecks:
- https://www.googleapis.com/compute/v1/projects/project_name/global/httpsHealthChecks/my-hc
id: 'SERVICE_ID'
kind: compute#backendService
loadBalancingScheme: EXTERNAL
name: my-service
port: 8443
portName: https
protocol: HTTPS
selfLink: https://www.googleapis.com/compute/v1/projects/project_name/global/backendServices/my-service
sessionAffinity: NONE
timeoutSec: 3610

Retrieve the user identity

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

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

Here is some sample code to secure an app with signed Cloud IAP headers:

Python

import jwt
import requests


def validate_iap_jwt_from_app_engine(iap_jwt, cloud_project_number,
                                     cloud_project_id):
    """Validate a JWT passed to your App Engine app by Identity-Aware Proxy.

    Args:
      iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.
      cloud_project_number: 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.
      cloud_project_id: The project *ID* for your Google Cloud project.

    Returns:
      (user_id, user_email, error_str).
    """
    expected_audience = '/projects/{}/apps/{}'.format(
        cloud_project_number, cloud_project_id)
    return _validate_iap_jwt(iap_jwt, expected_audience)


def validate_iap_jwt_from_compute_engine(iap_jwt, cloud_project_number,
                                         backend_service_id):
    """Validate an IAP JWT for your (Compute|Container) Engine service.

    Args:
      iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.
      cloud_project_number: 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.
      backend_service_id: 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.

    Returns:
      (user_id, user_email, error_str).
    """
    expected_audience = '/projects/{}/global/backendServices/{}'.format(
        cloud_project_number, backend_service_id)
    return _validate_iap_jwt(iap_jwt, expected_audience)


def _validate_iap_jwt(iap_jwt, expected_audience):
    try:
        key_id = jwt.get_unverified_header(iap_jwt).get('kid')
        if not key_id:
            return (None, None, '**ERROR: no key ID**')
        key = get_iap_key(key_id)
        decoded_jwt = jwt.decode(
            iap_jwt, key,
            algorithms=['ES256'],
            audience=expected_audience)
        return (decoded_jwt['sub'], decoded_jwt['email'], '')
    except (jwt.exceptions.InvalidTokenError,
            requests.exceptions.RequestException) as e:
        return (None, None, '**ERROR: JWT validation error {}**'.format(e))


def get_iap_key(key_id):
    """Retrieves a public key from the list published by Identity-Aware Proxy,
    re-fetching the key file if necessary.
    """
    key_cache = get_iap_key.key_cache
    key = key_cache.get(key_id)
    if not key:
        # Re-fetch the key file.
        resp = requests.get(
            'https://www.gstatic.com/iap/verify/public_key')
        if resp.status_code != 200:
            raise Exception(
                'Unable to fetch IAP keys: {} / {} / {}'.format(
                    resp.status_code, resp.headers, resp.text))
        key_cache = resp.json()
        get_iap_key.key_cache = key_cache
        key = key_cache.get(key_id)
        if not key:
            raise Exception('Key {!r} not found'.format(key_id))
    return key


# Used to cache the Identity-Aware Proxy public keys.  This code only
# refetches the file when a JWT is signed with a key not present in
# this cache.
get_iap_key.key_cache = {}

Java

import com.google.api.client.http.HttpRequest;
import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.net.URL;
import java.security.interfaces.ECPublicKey;
import java.time.Clock;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/** Verify IAP authorization JWT token in incoming request. */
public class VerifyIapRequestHeader {

  private static final String PUBLIC_KEY_VERIFICATION_URL =
      "https://www.gstatic.com/iap/verify/public_key-jwk";

  private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";

  // using a simple cache with no eviction for this sample
  private final Map<String, JWK> keyCache = new HashMap<>();

  private static Clock clock = Clock.systemUTC();

  private ECPublicKey getKey(String kid, String alg) throws Exception {
    JWK jwk = keyCache.get(kid);
    if (jwk == null) {
      // update cache loading jwk public key data from url
      JWKSet jwkSet = JWKSet.load(new URL(PUBLIC_KEY_VERIFICATION_URL));
      for (JWK key : jwkSet.getKeys()) {
        keyCache.put(key.getKeyID(), key);
      }
      jwk = keyCache.get(kid);
    }
    // confirm that algorithm matches
    if (jwk != null && jwk.getAlgorithm().getName().equals(alg)) {
      return ECKey.parse(jwk.toJSONString()).toECPublicKey();
    }
    return null;
  }

  // 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.
  boolean verifyJwtForAppEngine(HttpRequest request, long projectNumber, String projectId)
      throws Exception {
    // Check for iap jwt header in incoming request
    String jwt = request.getHeaders().getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
    if (jwt == null) {
      return false;
    }
    return verifyJwt(
        jwt,
        String.format("/projects/%s/apps/%s", Long.toUnsignedString(projectNumber), projectId));
  }

  boolean verifyJwtForComputeEngine(
      HttpRequest request, long projectNumber, long backendServiceId) throws Exception {
    // Check for iap jwt header in incoming request
    String jwtToken = request.getHeaders()
        .getFirstHeaderStringValue("x-goog-iap-jwt-assertion");
    if (jwtToken == null) {
      return false;
    }
    return verifyJwt(
        jwtToken,
        String.format(
            "/projects/%s/global/backendServices/%s",
            Long.toUnsignedString(projectNumber), Long.toUnsignedString(backendServiceId)));
  }

  private boolean verifyJwt(String jwtToken, String expectedAudience) throws Exception {

    // parse signed token into header / claims
    SignedJWT signedJwt = SignedJWT.parse(jwtToken);
    JWSHeader jwsHeader = signedJwt.getHeader();

    // header must have algorithm("alg") and "kid"
    Preconditions.checkNotNull(jwsHeader.getAlgorithm());
    Preconditions.checkNotNull(jwsHeader.getKeyID());

    JWTClaimsSet claims = signedJwt.getJWTClaimsSet();

    // claims must have audience, issuer
    Preconditions.checkArgument(claims.getAudience().contains(expectedAudience));
    Preconditions.checkArgument(claims.getIssuer().equals(IAP_ISSUER_URL));

    // claim must have issued at time in the past
    Date currentTime = Date.from(Instant.now(clock));
    Preconditions.checkArgument(claims.getIssueTime().before(currentTime));
    // claim must have expiration time in the future
    Preconditions.checkArgument(claims.getExpirationTime().after(currentTime));

    // must have subject, email
    Preconditions.checkNotNull(claims.getSubject());
    Preconditions.checkNotNull(claims.getClaim("email"));

    // verify using public key : lookup with key id, algorithm name provided
    ECPublicKey publicKey = getKey(jwsHeader.getKeyID(), jwsHeader.getAlgorithm().getName());

    Preconditions.checkNotNull(publicKey);
    JWSVerifier jwsVerifier = new ECDSAVerifier(publicKey);
    return signedJwt.verify(jwsVerifier);
  }
}

PHP

namespace Google\Cloud\Samples\Iap;

# Imports OAuth Guzzle HTTP libraries.
use GuzzleHttp\Client;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;

/**
 * Validate a JWT passed to your App Engine app by Identity-Aware Proxy.
 *
 * @param string $iap_jwt The contents of the X-Goog-IAP-JWT-Assertion header.
 * @param string $cloud_project_number 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 $cloud_project Your Google Cloud Project ID.
 *
 * @return (user_id, user_email).
 */
function validate_jwt_from_app_engine($iap_jwt, $cloud_project_number, $cloud_project_id)
{
    $expected_audience = sprintf(
        '/projects/%s/apps/%s',
        $cloud_project_number,
        $cloud_project_id
    );
    return validate_jwt($iap_jwt, $expected_audience);
}

/**
 * Validate a JWT passed to your Compute / Container Engine app by Identity-Aware Proxy.
 *
 * @param string $iap_jwt The contents of the X-Goog-IAP-JWT-Assertion header.
 * @param string $cloud_project_number 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 $backend_service_id 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.
 *
 * @return (user_id, user_email).
 */
function validate_jwt_from_compute_engine($iap_jwt, $cloud_project_number, $backend_service_id)
{
    $expected_audience = sprintf(
        '/projects/%s/global/backendServices/%s',
        $cloud_project_number,
        $backend_service_id
    );
    return validate_jwt($iap_jwt, $expected_audience);
}


function validate_jwt($iap_jwt, $expected_audience)
{
    // Validate the algorithm and kid headers. Also fetch the public key using the kid.
    $token = (new Parser())->parse((string) $iap_jwt); // Parses from a string
    $algorithm = $token->getHeader('alg');
    assert($algorithm =='ES256');
    $kid = $token->getHeader('kid');
    $client = new Client(['base_uri' => 'https://www.gstatic.com/']);
    $response = $client->request('GET', 'iap/verify/public_key');
    $body_content = json_decode((string) $response->getBody());
    $public_key = $body_content->$kid;

    // Validate token by checking issuer and audience fields. The JWT library automatically checks the time constraints.
    $data = new ValidationData();
    $data->setIssuer('https://cloud.google.com/iap');
    $data->setAudience($expected_audience);
    assert($token->validate($data));

    // Verify the signature using the JWT library.
    $signer = new Sha256();
    assert($token->verify($signer, $public_key));

    // Return the user identity (subject and user email) if JWT verification is successful.
    return array('sub' => $token->getClaim('sub'), 'email' => $token->getClaim('email'));
}

Create a health check exception

As mentioned previously, Compute Engine and Kubernetes Engine health checks don't use JWT headers and Cloud IAP doesn't handle health checks. You'll need to configure your health check and app to allow the health check access.

Configure the health check

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

  1. Go to the Cloud Platform Console Health checks page.
    Go to the Health checks page
  2. Click the health check you're using for your app, then click Edit.
  3. Under Request path add a non-sensitive path name.
  4. Click Save.

Configure the JWT validation

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

if HttpRequest.path_info = '/HEALTH_CHECK_PATH'
  return HttpResponse(status=200)
else
  VALIDATION_FUNCTION

Send feedback about...

Identity-Aware Proxy Documentation