Securing Your App with Signed Headers

This page describes how to secure your app with signed 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 Container 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.

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 header x-goog-iap-jwt-assertion. It's more secure than an unsigned x-goog-authenticated-user-{email,id} header.

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 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 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 Container 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 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);
  }
}

Monitor your resources on the go

Get the Google Cloud Console app to help you manage your projects.

Send feedback about...

Identity-Aware Proxy Documentation