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 risques suivants :

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

Pour sécuriser votre application, vous devez utiliser des en-têtes signés pour tous les types d'application.

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

Les vérifications d'état de Compute Engine et de GKE n'incluent pas les en-têtes JWT, et IAP ne traite pas les vérifications d'état. Si votre vérification de l'état d'état renvoie des erreurs d'accès, assurez-vous que vous l'avez correctement configurée dans la console Google Cloud et que votre validation d'en-tête JWT autorise le chemin de la vérification de l'état d'état. Pour en savoir plus, consultez Créer une vérification de l'état 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. Lorsque IAP est activé, il 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. Tout d'abord, récupérez 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.

IAP alterne régulièrement ses clés publiques. Pour vous assurer de pouvoir toujours valider les jetons JWT, consultez Automatiser la mise en cache des clés publiques.

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
  • Cloud Run : /projects/PROJECT_NUMBER/locations/REGION/services/SERVICE_NAME
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.

Lorsque vous spécifiez une règle relative aux appareils et que l'organisation a accès aux données de l'appareil, le DeviceId est également stocké dans l'objet JSON. Notez qu'une demande adressée à une autre organisation peut ne pas être autorisée à afficher les données de l'appareil.

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

Pour obtenir les valeurs de la chaîne aud depuis la console Google Cloud , 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.

Pour obtenir les valeurs de la chaîne aud à l'aide de l'outil de ligne de commande gcloud de la gcloud CLI, vous devez connaître l'ID du projet. Vous le trouverez dans la fiche Informations sur le projet de la consoleGoogle Cloud . Exécutez ensuite les commandes indiquées 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 précédentes réussissent, récupérez 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 Google.Apis.Auth.OAuth2;
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 },
            TrustedIssuers = { "https://cloud.google.com/iap" },
            CertificatesUrl = GoogleAuthConsts.IapKeySetUrl,
        };

        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: %w", 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: %w", 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 $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

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, 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 = 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 de l'état d'état, attribuez-lui un chemin non sensible à l'aide de la consoleGoogle Cloud . Veillez à ce qu'aucune autre ressource ne partage ce chemin.

  1. Accédez à la page Vérifications d'état de la console Google Cloud .
    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 utilisé par Google Cloud lors de l'envoi de requêtes de vérification de l'état'état. En cas d'omission, la requête de vérification de l'é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

Automatiser la mise en cache des clés publiques

IAP alterne régulièrement ses clés publiques. Pour vous assurer de toujours pouvoir valider le jeton JWT IAP, nous vous recommandons de mettre en cache les clés afin d'éviter de les récupérer à partir de l'URL publique pour chaque requête, et d'automatiser le processus de mise à jour de la clé mise en cache. Cette approche est particulièrement utile pour les applications qui s'exécutent dans un environnement avec des restrictions réseau, comme un périmètre VPC Service Controls.

Un périmètre VPC Service Controls peut empêcher l'accès direct à l'URL publique des clés. En mettant les clés en cache dans un bucket Cloud Storage, vos applications peuvent les récupérer à partir d'un emplacement situé dans votre périmètre VPC-SC.

La configuration Terraform suivante déploie une fonction sur Cloud Run qui récupère les dernières clés publiques IAP depuis https://www.gstatic.com/iap/verify/public_key-jwk et les stocke dans un bucket Cloud Storage. Une tâche Cloud Scheduler déclenche cette fonction toutes les 12 heures pour que les clés restent à jour.

Cette configuration comprend les éléments suivants :

  • Les API Google Cloud nécessaires sont activées pour utiliser Cloud Run et stocker et mettre en cache les clés.
  • Un bucket Cloud Storage pour stocker les clés publiques IAP récupérées
  • Un bucket Cloud Storage pour préparer le code source des fonctions Cloud Run
  • Comptes de service pour Cloud Run Functions et Cloud Scheduler avec les autorisations IAM appropriées
  • Fonction Python permettant de récupérer et de stocker des clés
  • Une tâche Cloud Scheduler pour déclencher la fonction toutes les 12 heures

Structure des répertoires

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

function_source/main.py

import functions_framework
import requests
from google.cloud import storage
import os

# Environment variables to be set in the function configuration
BUCKET_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.http
def update_iap_keys(request):
    """Fetches IAP public keys from the public URL and stores them in a Cloud Storage bucket."""
    if not BUCKET_NAME:
        print("Error: BUCKET_NAME environment variable not set.")
        return "BUCKET_NAME environment variable not set.", 500

    try:
        # Fetch the keys
        response = requests.get(IAP_KEYS_URL)
        response.raise_for_status()  # Raise an exception for bad status codes
        keys_content = response.text
        print(f"Successfully fetched keys from {IAP_KEYS_URL}")

        # Store in Cloud Storage
        storage_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}")
        return f"Successfully updated {OBJECT_NAME} in bucket {BUCKET_NAME}", 200

    except requests.exceptions.RequestException as e:
        print(f"Error fetching keys from {IAP_KEYS_URL}: {e}")
        return f"Error fetching keys: {e}", 500
    except Exception as e:
        print(f"Error interacting with Cloud Storage: {e}")
        return f"Error interacting with Cloud Storage: {e}", 500

Remplacez les éléments suivants :

  • BUCKET_NAME : nom de votre bucket Cloud Storage
  • OBJECT_NAME : nom de l'objet dans lequel stocker vos clés

function_source/requirements.txt

functions-framework==3.*
requests
google-cloud-storage

variables.tf

variable "project_id" {
  description = "The Google Cloud project ID."
  type        = string
  default     = PROJECT_ID
}

variable "region" {
  description = "The Google Cloud region."
  type        = string
  default     = "REGION"
}

variable "iap_keys_bucket_name" {
  description = "The name of the Cloud Storage bucket to store IAP keys."
  type        = string
  default     = BUCKET_NAME"
}

variable "function_source_bucket_name" {
  description = "The name of the Cloud Storage bucket to store the function source code."
  type        = string
  default     = "BUCKET_NAME_FUNCTION"
}

Remplacez les éléments suivants :

  • PROJECT_ID : ID de votre projet Google Cloud
  • REGION : région dans laquelle déployer les ressources, par exemple us-central1
  • BUCKET_NAME : nom du bucket Cloud Storage qui stocke les clés IAP
  • BUCKET_NAME_FUNCTION : nom du bucket Cloud Storage qui stocke le code source des fonctions Cloud Run

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_id
  region  = var.region
}

provider "google-beta" {
  project = var.project_id
  region  = var.region
}

# Enable necessary APIs
resource "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.key
  disable_on_destroy = false
}

# Cloud Storage Bucket to store the IAP public keys
resource "google_storage_bucket" "iap_keys_bucket" {
  name                        = var.iap_keys_bucket_name
  location                    = var.region
  uniform_bucket_level_access = true
  versioning {
    enabled = true
  }
  lifecycle {
    prevent_destroy = false # Set to true in production to prevent accidental deletion
  }
}

# Cloud Storage Bucket to store the Cloud Function source code
resource "google_storage_bucket" "function_source_bucket" {
  name                        = var.function_source_bucket_name
  location                    = var.region
  uniform_bucket_level_access = true
}

# Archive the function source code
data "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 bucket
resource "google_storage_bucket_object" "function_source_object" {
  name   = "function_source.zip"
  bucket = google_storage_bucket.function_source_bucket.name
  source = data.archive_file.function_source_zip.output_path
}

# Service Account for the Cloud Function
resource "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 bucket
resource "google_storage_bucket_iam_member" "keys_bucket_writer" {
  bucket = google_storage_bucket.iap_keys_bucket.name
  role   = "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-beta
  name     = "update-iap-keys-function"
  location = var.region

  build_config {
    runtime     = "python312"
    entry_point = "update_iap_keys"
    source {
      storage_source {
        bucket = google_storage_bucket.function_source_bucket.name
        object = google_storage_bucket_object.function_source_object.name
      }
    }
  }

  service_config {
    max_instance_count = 1
    available_memory   = "256M"
    timeout_seconds    = 60
    ingress_settings   = "ALLOW_ALL"
    service_account_email = google_service_account.iap_key_updater_sa.email
    environment_variables = {
      BUCKET_NAME = google_storage_bucket.iap_keys_bucket.name
      OBJECT_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 job
resource "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 Function
resource "google_cloudfunctions2_function_iam_member" "invoker" {
  provider       = google-beta
  project        = google_cloudfunctions2_function.update_iap_keys_func.project
  location       = google_cloudfunctions2_function.update_iap_keys_func.location
  cloud_function = google_cloudfunctions2_function.update_iap_keys_func.name
  role           = "roles/cloudfunctions.invoker"
  member         = "serviceAccount:${google_service_account.iap_key_scheduler_sa.email}"
}

# Cloud Scheduler Job
resource "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 hours
  time_zone   = "Etc/UTC"
  region      = var.region

  http_target {
    uri = google_cloudfunctions2_function.update_iap_keys_func.service_config[0].uri
    http_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

Créez un fichier terraform.tfvars pour spécifier l'ID de votre projet et personnaliser les noms de buckets si nécessaire :

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"

Déployer avec Terraform

  1. Enregistrez les fichiers dans la structure de répertoires décrite précédemment.
  2. Accédez au répertoire dans votre terminal et initialisez Terraform :
    terraform init
  3. Planifiez les modifications :
    terraform plan
  4. Appliquez les modifications :
    terraform apply

L'infrastructure est alors déployée. La tâche Cloud Scheduler déclenche la fonction toutes les 12 heures, récupère les clés IAP et les stocke dans gs://BUCKET_NAME/iap_public_keys.jwk par défaut. Vos applications peuvent désormais récupérer les clés de ce bucket.

Effectuer un nettoyage des ressources

Pour supprimer les ressources créées par Terraform, exécutez les commandes suivantes :

gsutil rm -a gs://BUCKET_NAME/**

terraform destroy -auto-approve

Remplacez BUCKET_NAME par le bucket Cloud Storage pour vos clés.

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, l'URL de sa photo et 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. 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.

Limites de taille des revendications IdP

Lorsqu'un utilisateur se connecte avec Identity Platform, les attributs utilisateur supplémentaires sont propagés à la charge utile du jeton d'ID Identity Platform sans état, qui est transmis de manière sécurisée à IAP. IAP émettra ensuite son propre cookie opaque sans état, qui contient également les mêmes revendications. IAP génère l'en-tête JWT signé en fonction du contenu du cookie.

Par conséquent, si une session est lancée avec de nombreuses revendications, elle peut dépasser la taille maximale autorisée pour les cookies, qui est généralement d'environ 4 Ko dans la plupart des navigateurs. L'opération de connexion échouera.

Assurez-vous que seules les revendications nécessaires sont propagées dans les attributs SAML ou OIDC du fournisseur d'identité. Une autre option consiste à utiliser des fonctions de blocage pour filtrer les revendications qui ne sont pas requises pour la vérification de l'autorisation.

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