Sécuriser votre application avec des en-têtes signés

Cette page explique comment sécuriser votre application avec des en-têtes IAP signés. Une fois configuré, Identity-Aware Proxy (IAP) utilise des jetons Web JSON (JWT, JSON Web Token) pour s'assurer qu'une requête envoyée à votre application est autorisée. Cette démarche protège votre application contre les types de risques suivants :

  • Désactivation accidentelle du service IAP
  • Pare-feu mal configurés
  • Accès depuis le projet

Afin de sécuriser correctement votre application, vous devez vous servir d'en-têtes signés qui conviennent à tous les types d'application.

Si vous disposez d'une application d'environnement standard App Engine, vous pouvez également utiliser l'API Users.

Sachez que les vérifications d'état de Compute Engine et de GKE n'incluent pas les en-têtes JWT, et qu'IAP ne gère pas ces vérifications. Si votre vérification d'état renvoie des erreurs d'accès, assurez-vous que vous l'avez correctement configurée dans Cloud Console et que votre validation d'en-tête JWT ajoute le chemin de la vérification d'état à la liste blanche. Pour en savoir plus, consultez la section Créer une exception de vérification d'état.

Avant de commencer

Pour sécuriser votre application avec des en-têtes signés, vous avez besoin des éléments suivants :

Sécuriser votre application avec des en-têtes IAP

Pour sécuriser votre application avec le JWT IAP, validez l'en-tête, la charge utile et la signature de celui-ci. Le JWT se trouve dans l'en-tête de requête HTTP x-goog-iap-jwt-assertion. En contournant IAP, un pirate informatique peut falsifier les en-têtes d'identité IAP non signés : x-goog-authenticated-user-{email,id}. Le JWT IAP constitue une alternative plus sécurisée.

Les en-têtes signés offrent une sécurité secondaire dans le cas d'un éventuel contournement d'IAP. Sachez que lorsqu'il est activé, IAP retire les en-têtes x-goog-* fournis par le client quand la requête passe par l'infrastructure de diffusion du service.

Valider l'en-tête JWT

Assurez-vous que l'en-tête JWT respecte les contraintes suivantes :

Revendications d'en-tête JWT
alg Algorithme ES256
kid ID de clé Doit correspondre à l'une des clés publiques répertoriées dans le fichier de clé IAP, disponible dans deux formats différents : https://www.gstatic.com/iap/verify/public_key et https://www.gstatic.com/iap/verify/public_key-jwk

Assurez-vous que le JWT a été signé par la clé privée correspondant à la revendication kid du jeton. Pour ce faire, récupérez d'abord la clé publique à l'un des deux emplacements suivants :

  • https://www.gstatic.com/iap/verify/public_key. Cette URL contient un dictionnaire JSON qui mappe les revendications kid avec les valeurs de la clé publique.
  • https://www.gstatic.com/iap/verify/public_key-jwk. Cette URL contient les clés publiques IAP au format JWK.

Une fois la clé publique récupérée, validez la signature à l'aide d'une bibliothèque JWT.

Valider la charge utile JWT

Assurez-vous que la charge utile JWT respecte les contraintes suivantes :

Revendications de charge utile JWT
exp Date/Heure d'expiration Il doit s'agir d'une date future. Le temps est mesuré en secondes depuis l'époque UNIX. Prévoyez 30 secondes de décalage. La durée de vie maximale d'un jeton est de 10 minutes + 2 * le décalage.
iat Date/Heure d'émission Il doit s'agir d'une date antérieure. Le temps est mesuré en secondes depuis l'époque UNIX. Prévoyez 30 secondes de décalage.
aud Cible Doit être une chaîne comportant les valeurs suivantes :
  • App Engine : /projects/PROJECT_NUMBER/apps/PROJECT_ID
  • Compute Engine et GKE : /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID
iss Émetteur Doit être https://cloud.google.com/iap.
hd Domaine du compte Si un compte appartient à un domaine hébergé, la revendication hd est fournie afin de différencier le domaine auquel le compte est associé.
google Revendication Google Si un ou plusieurs niveaux d'accès s'appliquent à la requête, leur nom est stocké sous forme de tableau de chaînes dans l'objet JSON de la revendication google, sous la clé access_levels.

Vous pouvez obtenir les valeurs de la chaîne aud mentionnée ci-dessus en accédant à Cloud Console, ou vous pouvez utiliser l'outil de ligne de commande gcloud.

Pour obtenir les valeurs de la chaîne aud depuis Cloud Console, accédez aux paramètres Identity-Aware Proxy de votre projet, cliquez sur Plus à côté de la ressource de l'équilibreur de charge, puis sélectionnez Signed Header JWT Audience (Audience de jeton JWT avec en-tête signé). La boîte de dialogue Jeton JWT avec en-tête signé qui apparaît affiche la revendication aud pour la ressource sélectionnée.

menu à développer comportant l'option

Pour obtenir les valeurs de la chaîne aud à l'aide de l'outil de ligne de commande gcloud du SDK Cloud, vous devez connaître l'ID du projet. Vous le trouverez dans la fiche Informations sur le projet de Cloud Console. Exécutez ensuite les commandes indiquées ci-dessous pour chaque valeur.

Numéro du projet

Pour obtenir votre numéro de projet à l'aide de l'outil de ligne de commande gcloud, exécutez la commande suivante :

gcloud projects describe PROJECT_ID

La commande renvoie un résultat semblable à celui-ci :

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

ID du service

Pour obtenir votre ID de service à l'aide de l'outil de ligne de commande gcloud, exécutez la commande suivante :

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

La commande renvoie un résultat semblable à celui-ci :

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

Récupérer l'identité de l'utilisateur

Si toutes les vérifications ci-dessus réussissent, vous pouvez récupérer l'identité de l'utilisateur. La charge utile du jeton d'ID contient les informations utilisateur suivantes :

Identité de l'utilisateur de la charge utile du jeton d'ID
sub Objet Identifiant unique et stable pour l'utilisateur. Utilisez cette valeur à la place de l'en-tête x-goog-authenticated-user-id.
email Adresse e-mail de l'utilisateur Adresse e-mail de l'utilisateur.
  • Utilisez cette valeur à la place de l'en-tête x-goog-authenticated-user-email.
  • Contrairement à cet en-tête et à la revendication sub, cette valeur ne comporte pas de préfixe d'espace de noms.

Vous trouverez ci-dessous un exemple de code permettant de sécuriser une application avec des en-têtes IAP signés :

C#


using Google.Apis.Auth;
using System;
using System.Threading;
using System.Threading.Tasks;

public class IAPTokenVerification
{
    /// <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>
    public async Task<JsonWebSignature.Payload> VerifyTokenAsync(
        string signedJwt, string expectedAudience, CancellationToken cancellationToken = default)
    {
        SignedTokenVerificationOptions options = new SignedTokenVerificationOptions
        {
            // 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 }
        };

        return await JsonWebSignature.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.
func validateJWTFromAppEngine(w io.Writer, iapJWT, projectNumber, projectID string) 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)
	if err != nil {
		return fmt.Errorf("idtoken.Validate: %v", err)
	}

	// payload contains the JWT claims for further inspection or validation
	fmt.Fprintf(w, "payload: %v", payload)

	return nil
}

// validateJWTFromComputeEngine validates a JWT found in the
// "x-goog-iap-jwt-assertion" header.
func validateJWTFromComputeEngine(w io.Writer, iapJWT, projectNumber, backendServiceID string) 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)
	if err != nil {
		return fmt.Errorf("idtoken.Validate: %v", err)
	}

	// payload contains the JWT claims for further inspection or validation
	fmt.Fprintf(w, "payload: %v", payload)

	return nil
}

Java


import com.google.api.client.http.HttpRequest;
import com.google.api.client.json.webtoken.JsonWebToken;
import com.google.auth.oauth2.TokenVerifier;

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

  private static final String IAP_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.
  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) {
    TokenVerifier tokenVerifier = TokenVerifier.newBuilder()
        .setAudience(expectedAudience)
        .setIssuer(IAP_ISSUER_URL)
        .build();
    try {
      JsonWebToken jsonWebToken = tokenVerifier.verify(jwtToken);

      // Verify that the token contain subject and email claims
      JsonWebToken.Payload payload = jsonWebToken.getPayload();
      return payload.getSubject() != null && payload.get("email") != null;
    } catch (TokenVerifier.VerificationException e) {
      System.out.println(e.getMessage());
      return false;
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const iapJwt = 'SOME_ID_TOKEN'; // JWT from the "x-goog-iap-jwt-assertion" header

let expectedAudience = null;
if (projectNumber && projectId) {
  // Expected Audience for App Engine.
  expectedAudience = `/projects/${projectNumber}/apps/${projectId}`;
} else if (projectNumber && backendServiceId) {
  // Expected Audience for Compute Engine
  expectedAudience = `/projects/${projectNumber}/global/backendServices/${backendServiceId}`;
}

const oAuth2Client = new OAuth2Client();

async function verify() {
  // Verify the id_token, and access the claims.
  const response = await oAuth2Client.getIapPublicKeys();
  const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
    iapJwt,
    response.pubkeys,
    expectedAudience,
    ['https://cloud.google.com/iap']
  );
  // Print out the info contained in the IAP ID token
  console.log(ticket);
}

verify().catch(console.error);

PHP

namespace Google\Cloud\Samples\Iap;

# Imports Google auth libraries for IAP validation
use Google\Auth\AccessToken;

/**
 * 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 signature using the IAP cert URL.
    $token = new AccessToken();
    $jwt = $token->verify($iap_jwt, [
        'certsLocation' => AccessToken::IAP_CERT_URL
    ]);

    if (!$jwt) {
        return print('Failed to validate JWT: Invalid JWT');
    }

    // Validate token by checking issuer and audience fields.
    assert($jwt['iss'] == 'https://cloud.google.com/iap');
    assert($jwt['aud'] == $expected_audience);

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

Python

from google.auth.transport import requests
from google.oauth2 import id_token

def validate_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'], '')
    except Exception as e:
        return (None, None, '**ERROR: JWT validation error {}**'.format(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 = nil
if project_number && project_id
  # Expected audience for App Engine
  audience = "/projects/#{project_number}/apps/#{project_id}"
elsif project_number && backend_service_id
  # Expected audience for Compute Engine
  audience = "/projects/#{project_number}/global/backendServices/#{backend_service_id}"
end

# The client ID as the target audience for IAP
payload = Google::Auth::IDTokens.verify_iap iap_jwt, aud: audience

puts payload

if audience.nil?
  puts "Audience not verified! Supply a project_number and project_id to verify"
end

Tester le code de validation

Si vous accédez à votre application à l'aide des paramètres de requête secure_token_test, IAP inclut un JWT non valide. Utilisez-le pour vous assurer que la logique de validation des JWT gère tous les cas d'échec et pour connaître le comportement de votre application lorsqu'elle reçoit un JWT non valide.

Créer une exception de vérification d'état

Comme mentionné précédemment, les vérifications d'état de Compute Engine et de GKE n'utilisent pas les en-têtes JWT, et IAP ne gère pas ces vérifications. Vous devez configurer votre vérification d'état et votre application pour autoriser l'accès à ces vérifications.

Configurer la vérification d'état

Si vous n'avez pas encore défini de chemin pour votre vérification d'état, attribuez-lui un chemin non sensible à l'aide de Cloud Console. Veillez à ce qu'aucune autre ressource ne partage ce chemin.

  1. Accédez à la page Vérifications d'état de Cloud Console.
    Accéder à la page "Vérifications d'état"
  2. Cliquez sur la vérification d'état que vous utilisez pour votre application, puis sur Modifier.
  3. Dans le champ Chemin de requête, ajoutez un nom de chemin non sensible. Cela permet de spécifier le chemin d'URL que Google Cloud utilise lors de l'envoi de requêtes de vérification d'état. En cas d'omission, la requête de vérification d'état est envoyée à /.
  4. Cliquez sur Save.

Configurer la validation JWT

Dans votre code qui appelle le processus de validation JWT, ajoutez une condition pour renvoyer un code 200 à votre chemin de vérification d'état. Exemple :

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

Les JWT pour les identités externes

Si vous utilisez IAP avec des identités externes, IAP émet tout de même un JWT signé à chaque requête authentifiée, tout comme avec les identités Google. Il existe toutefois quelques différences.

Informations sur le fournisseur

Lorsque vous utilisez des identités externes, la charge utile JWT contient une revendication nommée gcip. Cette revendication contient des informations sur l'utilisateur, telles que son adresse e-mail et l'URL de sa photo, ainsi que tous les autres attributs propres au fournisseur.

Voici un exemple de JWT pour un utilisateur qui s'est connecté avec 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"
}',

Champs email et sub

Si un utilisateur a été authentifié par Identity Platform, les champs email et sub du JWT sont préfixés avec l'émetteur de jeton Identity Platform et l'ID de locataire utilisé (le cas échéant). Exemple :

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

Contrôler l'accès avec sign_in_attributes

IAM n'est pas compatible avec les identités externes, mais vous pouvez contrôler l'accès à l'aide de revendications intégrées dans le champ sign_in_attributes à la place. Prenons l'exemple d'un utilisateur qui s'est connecté avec un fournisseur SAML :

{
  "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"
}

Vous pouvez ajouter à votre application une logique semblable au code ci-dessous pour restreindre l'accès aux utilisateurs disposant d'un rôle valide :

const gcipClaims = JSON.parse(decodedIapJwtClaims.gcip);
if (gcipClaims &&
    gcipClaims.firebase &&
    gcipClaims.firebase.sign_in_attributes &&
    gcipClaims.firebase.sign_in_attribute.role === 'admin') {
  // Allow access to admin restricted resource.
} else {
  // Block access.
}

Vous pouvez accéder à d'autres attributs utilisateur des fournisseurs SAML et OIDC Identity Platform à l'aide de la revendication imbriquée gcipClaims.gcip.firebase.sign_in_attributes.