Authenticating using Google OpenID Connect Tokens

2019-07-19

This section covers authenticating against security perimeters which requires clients present valid OpenID Connect tokens. These security perimeters do not protect Google APIs but your services deployed behind certain Google Cloud Products. For example, if you deploy to Cloud Functions or an application on Cloud Run, you can enable a perimeter such that any client that wants to invoke the function or your application must present an ID token issued by Google.

These tokens are not Oauth2 access_tokens you would use to call a Google Service or API directly such as a Google Compute Engine API or Cloud Storage Bucket but id_tokens that assert identity and are signed by Google.

service for id_tokens


You can find the source here


What is an id_token?

OpenIDConnect (OIDC) tokens are signed JSON Web Tokens JWT used to assert identity and do not necessarily carry any implicit authorization against a resource. These tokens will just declare who the caller is and any service that the token is sent to can verify the token’s integrity by verifying the signature payload provided with the JWT. For more information, see the links in the References section below

If the ID Token is signed and issued by Google, that token can be used as a token against GCP service perimeters because the service can decode the token, verify its signature and finally identify the caller using values within the JWT claim. For example, the JWT header and payload below describes a token that was issued by google ("iss": "https://accounts.google.com"), identifies the caller ("email": "svc_account@.project.gserviceaccount.com"), has not expired (the service will check the exp: field), and finally will verify the JWT is intended for the service or not to "aud": "https://example.com".

    {
    "alg": "RS256",
    "kid": "5d887f26ce32577c4b5a8a1e1a52e19d301f8181",
    "typ": "JWT"
    }.
    {
    "aud": "https://example.com",
    "azp": "107145139691231222712",
    "email": "svc_account@.project.gserviceaccount.com",
    "email_verified": true,
    "exp": 1556665461,
    "iat": 1556661861,
    "iss": "https://accounts.google.com",
    "sub": "107145139691231222712"
    }

Note: the sub claim in the token above represents the unique internal Google identifier account representing the ID Token.

Whats an Audience?

The aud: field describes the service name this token was created to invoke. If a service receives an id_token, it must verify its integrity (signature), validity (is it expired) and if the aud: field is the predefined name it expects to see. If the names do not match, the service should reject the token as it could be a replay intended for another system.

Both Google Service Accounts and Users can get id_tokens but with an important distinction: User login oauth flows issue id_tokens statically bound to the web or oauth2 client_id the flow as associated with. That is, if a user logs into a web application involving oauth2, the id_token that the provider issues to the browser will have the aud: field bound to the oauth2 client_id.

Service Accounts on the other hand, can participate in a flow where it can receive and id_token from google with an aud: field it specified earlier. These token types issued by Service Accounts are generally the ones discussed in this article.

Sources of Google Issued ID Tokens

There are several ways to get a Google-issued id_token for a Service Account

Service Account JSON certificate

If you have a Google-issued Service account certificate file locally, you can sign the JWT with specific claims and exchange that with google to get a google-issued id_token. While specifying the claims to sign, a predefined claim called target_audience which when set will be used by Google oauth endpoint and reinterpreted as the aud: field in the id_token.

The flow using a json is:

  • Use the service account JSON file to sign a JWT with intended final audience set as target_audience.
  • Exchange the signed JWT with Google token endpoint: https://oauth2.googleapis.com/token
  • Google will verify the signature and identify the aller as the Service Account (since the caller had possession of the private key), then issue an id_token with the aud: field set to what the target_audience was set.
  • Return the id_token in the response back to the client.

Metadata Server

If a metadata server is available while running on Compute Engine, Appengine 2nd Generation, Cloud Functions or even Kubernetes engine, getting an id_token is simple: query the server itself for the token and provide the audience field the token should be for.

For example, the following curl command on any platform with metadata server will return an id_token:

curl -s-H 'Metadata-Flavor: Google' http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://example.com`

IAMCredentials generateIdToken()

Google Cloud IAM Credentials API provides a way for one service account to generate short lived tokens on behalf of another. One of the token types it can issue is an id_token via the generateIdToken() endpoint. Making Authorized Requests Once you have an id_token, provide that in the request Authorization header as:

Authorization: Bearer id_token

eg.

curl -v -H "Authorization: Bearer id_token" https://some-cloud-run-uc.a.run.app

Workload Identity Federation

See Getting GCP IDTokens using Workload Identity Federation

Services Accepting OIDC tokens for authentication

The following platforms use Google OIDC tokens for access controls. If you deploy an application behind any of of these services, you can optionally enable IAM access controls. What that will do is require any inbound access to a service to provide a valid Google OIDC token.

Furthermore, the token must have its aud: field set to the service name being invoked. For example, to invoke a Cloud Run service, you must setup IAM access for the users (see Managing access via IAM and any ID token provided must have be signed with the aud: field set to the service name itself. If the Cloud Run service is https://svc.-hash-.zone.cloud.run, the audience field must be set to the same

You can also deploy your own service outside of these services and verifying an OpenID Connect token. In this mode, your application that receives an OIDC token will need to manually verify its validity and audience field. You can use application frameworks like to do this like Spring Security, proxies like Envoy or even higher level Services like Istio.

For detailed implementation, see:

Services that include OIDC tokens in webhooks

Other services also support automatically including an OIDC token along with a webhook request

For example, you can configure Cloud Scheduler to emit an OIDC token with a preset audience. When a scheduled tasks fires, an http webhook url will be called and within the header payload, the OIDC token will get transmitted within the Authorization header. The webhook target can be your own application or any of the services listed in the previous section. If your application is running outside of these services listed under Services Accepting OIDC tokens for authentication, you will need to parse and verify the OIDC token.

See:

For detailed implementation, see:

How to get an ID Token

There are several flows to get an ID Token available. The snippets below demonstrate how to

  1. Get an IDToken
  2. Verify an IDToken
  3. Issue an authenticated request using the IDToken

Each while using

  • Service Account JSON certificate
  • Compute Engine Credentials

Disclaimer: the following snippets potentially use 3rd party libraries and is not supported by Google

gcloud

  • ServiceAccount
 gcloud auth activate-service-account --key-file=/path/to/svc_account.json
 gcloud auth print-identity-token --audience=https://example.com
  • ComputeEngine
 gcloud auth print-identity-token --audience=https://example.com
  • ImpersonatedCredentials
 gcloud auth print-identity-token --audiences=https://example.com --impersonate-service-account impersonated-account@projectID.iam.gserviceaccount.com

Cloud SDK

import google.oauth2.credentials
from google.oauth2 import id_token
from google.oauth2 import service_account
import google.auth
import google.auth.transport.requests
from google.auth.transport.requests import AuthorizedSession
from google.auth import compute_engine 

# pip install google-auth requests

target_audience = 'https://example.com'

url = 'https://httpbin.org/get'
certs_url='https://www.googleapis.com/oauth2/v1/certs'
metadata_identity_doc_url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"

svcAccountFile = '/path/to/svcaccount.json'

def GetIDTokenFromServiceAccount(svcAccountFile, target_audience):
  creds = service_account.IDTokenCredentials.from_service_account_file(
        svcAccountFile,
        target_audience= target_audience)
  request = google.auth.transport.requests.Request()
  creds.refresh(request)
  return creds.token

def GetIDTokenFromComputeEngine(target_audience):
  request = google.auth.transport.requests.Request()
  #creds = compute_engine.IDTokenCredentials(request=request, target_audience=target_audience, use_metadata_identity_endpoint=True)
  #creds.refresh(request)
  #return creds.token

  token = id_token.fetch_id_token(request, target_audience)
  return token

def VerifyIDToken(token, certs_url,  audience=None):
   request = google.auth.transport.requests.Request()
   result = id_token.verify_token(token,request,certs_url=certs_url)
   if audience in result['aud']:
     return True
   return False

def MakeAuthenticatedRequest(id_token, url):
  creds = google.oauth2.credentials.Credentials(id_token)
  authed_session = AuthorizedSession(creds)
  r = authed_session.get(url)
  print(r.status_code)
  print(r.text)

# For ServiceAccount
token = GetIDTokenFromServiceAccount(svcAccountFile,target_audience)

# For Compute Engine
#token = GetIDTokenFromComputeEngine(target_audience)

print('Token: ' + token)
if VerifyIDToken(token=token,certs_url=certs_url, audience=target_audience):
  print('token Verified with aud: ' + target_audience)
print('Making Authenticated API call:')
MakeAuthenticatedRequest(token,url)

# to verify ECDSA use certificate certs_url, not JWK
# print(VerifyIDToken(token=token,certs_url='https://www.gstatic.com/iap/verify/public_key', audience="/projects/248066739582/apps/fabled-ray-104117"))
package main

import (
	"context"
	"errors"
	"io/ioutil"
	"log"
	"net/http"

	jwt "github.com/golang-jwt/jwt/v4"
	"github.com/lestrrat/go-jwx/jwk"
	"google.golang.org/api/idtoken"
)

const ()

var (
	jwtSet *jwk.Set
)

// https://pkg.go.dev/google.golang.org/api@v0.23.0/idtoken

func main() {

	aud := "https://your.endpoint.run.url"
	url := "https://httpbin.org/get"
	jsonCert := "/path/to/svcaccount.json"

	ctx := context.Background()

	// With TokenSource
	// With ADC
	//ts, err := idtoken.NewTokenSource(ctx, aud)
	// With ServiceAccount
	ts, err := idtoken.NewTokenSource(ctx, aud, idtoken.WithCredentialsFile(jsonCert))
	if err != nil {
		log.Fatalf("unable to create TokenSource: %v", err)
	}
	tok, err := ts.Token()
	if err != nil {
		log.Fatalf("unable to retrieve Token: %v", err)
	}

	// the following snippet verifies an id token.
	// this step is done on the  receiving end of the oidc endpoint
	// adding this step in here as just as a demo on how to do this
	// google.golang.org/api/idtoken has built in verification capability for google issued tokens
	log.Printf("IDToken: %s", tok.AccessToken)
	validTok, err := idtoken.Validate(ctx, tok.AccessToken, aud)
	if err != nil {
		log.Fatalf("token validation failed: %v", err)
	}
	if validTok.Audience != aud {
		log.Fatalf("got %q, want %q", validTok.Audience, aud)
	}

	// If you want to verify any other issuer, first get the JWK endpoint,
	// in the following we are validating google's tokens, meaning its equivalent to the bit above
	// this is added in as an example of verifying IAP or other token types
	jwksURL := "https://www.googleapis.com/oauth2/v3/certs"
	jwtSet, err = jwk.FetchHTTP(jwksURL)
	if err != nil {
		log.Fatal("Unable to load JWK Set: ", err)
	}
	doc, err := verifyGoogleIDToken(ctx, tok.AccessToken)

	log.Printf("Verified Token: %v", doc)
	// End verification

	// With Authorized Client
	client, err := idtoken.NewClient(ctx, aud, idtoken.WithCredentialsFile(jsonCert))

	if err != nil {
		log.Fatalf("Could not generate NewClient: %v", err)
	}

	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		log.Fatalf("Error Creating HTTP Request: %v", err)
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Error making authenticated call: %v", err)
	}
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Error Reading response body: %v", err)
	}
	bodyString := string(bodyBytes)
	log.Printf("Authenticated Response: %v", bodyString)

}

// to verify OID token from GCE:

type gcpIdentityDoc struct {
	Google struct {
		ComputeEngine struct {
			InstanceCreationTimestamp int64  `json:"instance_creation_timestamp,omitempty"`
			InstanceID                string `json:"instance_id,omitempty"`
			InstanceName              string `json:"instance_name,omitempty"`
			ProjectID                 string `json:"project_id,omitempty"`
			ProjectNumber             int64  `json:"project_number,omitempty"`
			Zone                      string `json:"zone,omitempty"`
		} `json:"compute_engine"`
	} `json:"google"`
	Email           string `json:"email,omitempty"`
	EmailVerified   bool   `json:"email_verified,omitempty"`
	AuthorizedParty string `json:"azp,omitempty"`
	jwt.StandardClaims
}

func getKey(token *jwt.Token) (interface{}, error) {
	keyID, ok := token.Header["kid"].(string)
	if !ok {
		return nil, errors.New("expecting JWT header to have string kid")
	}
	if key := jwtSet.LookupKeyID(keyID); len(key) == 1 {
		log.Printf("     Found OIDC KeyID  " + keyID)
		return key[0].Materialize()
	}
	return nil, errors.New("unable to find key")
}

func verifyGoogleIDToken(ctx context.Context, rawToken string) (gcpIdentityDoc, error) {
	token, err := jwt.ParseWithClaims(rawToken, &gcpIdentityDoc{}, getKey)
	if err != nil {
		log.Printf("     Error parsing JWT %v", err)
		return gcpIdentityDoc{}, err
	}
	if claims, ok := token.Claims.(*gcpIdentityDoc); ok && token.Valid {
		log.Printf("     OIDC doc has Audience [%s]   Issuer [%s] and SubjectEmail [%s]", claims.Audience, claims.StandardClaims.Issuer, claims.Email)
		return *claims, nil
	}
	return gcpIdentityDoc{}, errors.New("Error parsing JWT Claims")
}
package com.test;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import com.google.auth.oauth2.ImpersonatedCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;

public class Main {

     private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
     private static final String credFile = "/path/to/svc.json";
     private static final String target_audience = "https://foo.com";

     public static void main(String[] args) throws Exception {

          Main tc = new Main();

          // IdTokenCredentials tok = tc.getIDTokenFromComputeEngine(target_audience);

          ServiceAccountCredentials sac = ServiceAccountCredentials.fromStream(new FileInputStream(credFile));
          sac = (ServiceAccountCredentials) sac.createScoped(Arrays.asList(CLOUD_PLATFORM_SCOPE));

          IdTokenCredentials tok = tc.getIDTokenFromServiceAccount(sac, target_audience);

          // String impersonatedServiceAccount =
          // "impersonated-account@project.iam.gserviceaccount.com";
          // IdTokenCredentials tok =
          // tc.getIDTokenFromImpersonatedCredentials((GoogleCredentials)sac,
          // impersonatedServiceAccount, target_audience);

          System.out.println("Making Authenticated API call:");
          String url = "https://httpbin.org/get";
          tc.MakeAuthenticatedRequest(tok, url);

          // the following snippet verifies an id token.
          // this step is done on the receiving end of the oidc endpoint
          // adding this step in here as just as a demo on how to do this

          System.out.println("Verifying Token:");
          System.out.println(Main.verifyGoogleToken(tok.getAccessToken().getTokenValue(), target_audience));

          // If you want to verify any other issuer, first get the JWK endpoint,
          // in the following we are validating google's tokens, meaning its equivalent to
          // the bit above
          // this is added in as an example of verifying IAP or other token types

          System.out.println("Verifying Token:");
          String jwkUrl = "https://www.googleapis.com/oauth2/v3/certs";
          System.out.println(Main.verifyToken(tok.getAccessToken().getTokenValue(), target_audience, jwkUrl));

     }

     public IdTokenCredentials getIDTokenFromServiceAccount(ServiceAccountCredentials saCreds, String targetAudience) {
          IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder().setIdTokenProvider(saCreds)
                    .setTargetAudience(targetAudience).build();
          return tokenCredential;
     }

     public IdTokenCredentials getIDTokenFromComputeEngine(String targetAudience) {
          ComputeEngineCredentials caCreds = ComputeEngineCredentials.create();
          IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder().setIdTokenProvider(caCreds)
                    .setTargetAudience(targetAudience)
                    .setOptions(Arrays.asList(IdTokenProvider.Option.FORMAT_FULL, IdTokenProvider.Option.LICENSES_TRUE))
                    .build();
          return tokenCredential;
     }

     public IdTokenCredentials getIDTokenFromImpersonatedCredentials(GoogleCredentials sourceCreds,
               String impersonatedServieAccount, String targetAudience) {
          ImpersonatedCredentials imCreds = ImpersonatedCredentials.create(sourceCreds, impersonatedServieAccount, null,
                    Arrays.asList(CLOUD_PLATFORM_SCOPE), 300);
          IdTokenCredentials tokenCredential = IdTokenCredentials.newBuilder().setIdTokenProvider(imCreds)
                    .setTargetAudience(targetAudience).setOptions(Arrays.asList(IdTokenProvider.Option.INCLUDE_EMAIL))
                    .build();
          return tokenCredential;
     }

     public static boolean verifyGoogleToken(String id_token, String audience) throws Exception {
          GsonFactory jsonFactory = new GsonFactory();
          GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), jsonFactory)
                    .setAudience(Collections.singletonList(audience)).build();
          GoogleIdToken idToken = verifier.verify(id_token);
          if (idToken != null) {
               Payload payload = idToken.getPayload();
               return true;
          } else {
               return false;
          }
     }

     public static boolean verifyToken(String id_token, String audience, String jwkUrl) throws Exception {

          DecodedJWT jwt = JWT.decode(id_token);
          if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) {
               System.out.println("Expired token");
               return false;
          }
          JwkProvider provider = new UrlJwkProvider(new java.net.URL(jwkUrl));
          Jwk jwk = provider.get(jwt.getKeyId());
          Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
          //Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) jwk.getPublicKey(),null);

          JWTVerifier verifier = JWT.require(algorithm).withAudience(audience).build();

          try {
               jwt = verifier.verify(id_token);
          } catch (SignatureVerificationException se) {
               System.out.println("Could not verify Signature: " + se.getMessage());
               return false;
          }
          return true;

     }

     public void MakeAuthenticatedRequest(IdTokenCredentials id_token, String url) throws IOException {

          GenericUrl genericUrl = new GenericUrl(url);
          HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(id_token);
          HttpTransport transport = new NetHttpTransport();
          HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
          request.setThrowExceptionOnExecuteError(false);
          HttpResponse response = request.execute();
          String r = response.parseAsString();
          System.out.println(r);

          System.out.println(id_token.getAccessToken().getTokenValue());
          response = request.execute();
          r = response.parseAsString();
          System.out.println(r);
     }
}
const {GoogleAuth, OAuth2Client,IdTokenClient} = require('google-auth-library');

const audience = 'https://example.com';
const url = 'https://httpbin.org/get';

const certs_url='https://www.googleapis.com/oauth2/v1/certs'


async function verifyGoogleIDToken(token, audience, url) {
  const id_client = new IdTokenClient(audience);
  const ticket = await id_client.verifyIdToken({
    idToken: token,
    audience: audience,
  });
  return true;
}

async function verifyIAPIDToken(token, audience) {
  const client = new OAuth2Client();
  const issuer = 'https://cloud.google.com/iap';
  const response = await client.getIapPublicKeys();
  const ticket = await client.verifySignedJwtWithCertsAsync(
   token,
   response.pubkeys,
   audience,
   [issuer]
  );
  const payload = ticket.getPayload();
  console.log('Verified with payload ' + payload);
  return true;
}

async function verifyIDToken(token, issuer, audience, jwkURL) {
  var jwt = require('jsonwebtoken');
  var jwksClient = require('jwks-rsa');
  var client = jwksClient({
    jwksUri: jwkURL
  });
  function getKey(header, callback){
    client.getSigningKey(header.kid, function(err, key) {
      var signingKey = key.publicKey || key.rsaPublicKey;
      callback(null, signingKey);
    });
  }
  var options = {
    algorithm: 'RS256',
    issuer: issuer,
    audience: audience
  }
  jwt.verify(token, getKey, options, function(err, decoded) {
    if (err){
      console.log("Error Verifying "  + err);
      return false
    }
    console.log(decoded)
    return true
  });

  return false;
}

async function main() {
  
 // const auth = new GoogleAuth();
  const auth = new GoogleAuth({
    keyFile: '/path/to/svc.json',
  });

  const client = await auth.getIdTokenClient(
    audience
  );
  const res = await client.request({
    method: 'GET',
    url: url,
  });
  console.log(res.data);

  console.log(client.credentials.id_token);

  let validated = await verifyGoogleIDToken(client.credentials.id_token,audience,certs_url);
  if (validated) {
    console.log("id_token validated with audience " + audience);
  }

  // const iap_id_token = '';
  // const iap_audience = '/projects/248066739582/apps/fabled-ray-104117';
  // let validated = await verifyIAPIDToken(iap_id_token,iap_audience);
  // if (validated) {
  //   console.log("id_token validated with audience " + audience);
  // }

  // const generic_id_token = 'eyJhb...';
  // const generic_endpoint = 'https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/jwks.json';
  // const generic_issuer = 'foo.bar';
  // const generic_audience = 'sal';
  // let validated = await verifyIDToken(generic_id_token,generic_issuer, generic_audience,generic_endpoint);
  // if (validated) {
  //   console.log("id_token validated with audience " + audience);
  // }  

}

main().catch(console.error);
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

using Google.Apis.Auth;
using Google.Apis.Http;
using Google.Apis.Auth.OAuth2;
using System.Net.Http;
using System.Net.Http.Headers;
using Google.Apis.Logging;
namespace Program
{
    public class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            try
            {                  
                //Google.ApplicationContext.RegisterLogger(new ConsoleLogger(LogLevel.All,true));
                var targetAudience = "https://myapp-6w42z6vi3q-uc.a.run.app";
                string uri = "https://httpbin.org/get";
                string CREDENTIAL_FILE_JSON = "/path/to/svc_accuont.json";
                new Program().Run(targetAudience, CREDENTIAL_FILE_JSON, uri).Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var err in ex.InnerExceptions)
                {
                    Console.WriteLine("ERROR: " + err.Message);
                }
            }

        }


        public async Task<string> Run(string targetAudience, string credentialsFilePath, string uri)
        {
            ServiceAccountCredential saCredential;

            using (var fs = new FileStream(credentialsFilePath, FileMode.Open, FileAccess.Read))
            {
                saCredential = ServiceAccountCredential.FromServiceAccountData(fs);
            }
            OidcToken oidcToken = await saCredential.GetOidcTokenAsync(OidcTokenOptions.FromTargetAudience(targetAudience).WithTokenFormat(OidcTokenFormat.Standard)).ConfigureAwait(false);            
            string token = await oidcToken.GetAccessTokenAsync().ConfigureAwait(false);

            // the following snippet verifies an id token. 
            // this step is done on the  receiving end of the oidc endpoint 
            // adding this step in here as just as a demo on how to do this
            //var options = SignedTokenVerificationOptions.Default;
            SignedTokenVerificationOptions options = new SignedTokenVerificationOptions
            {
                IssuedAtClockTolerance = TimeSpan.FromMinutes(1),
                ExpiryClockTolerance = TimeSpan.FromMinutes(1),
                TrustedAudiences = { targetAudience },
                CertificatesUrl = "https://www.googleapis.com/oauth2/v3/certs"  // default value
            };
            var payload = await JsonWebSignature.VerifySignedTokenAsync(token, options);
            Console.WriteLine("Verified with audience " + payload.Audience);
            // end verification

            // use the token
            using (var httpClient = new HttpClient())
            {               
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                string response = await httpClient.GetStringAsync(uri).ConfigureAwait(false);
                Console.WriteLine(response);
                return response;
            }
        }
    }
}

WARNING the code below is just something i wrote since Google Cloud SDK in CPP does not support managing id_tokens:

see google-cloud-cpp/issues/2786

be very careful!

#include <iostream>
#include <cstdlib>
#include <fstream>
#include <sstream>

#include <jwt-cpp/jwt.h>
#include <curl/curl.h>

/*

Generates and verifies google id_token on GCE instance or with GCP service account json file

// https://github.com/salrashid123/google_id_token

apt-get install libcurl4-openssl-dev libssl-dev

git clone https://github.com/Thalhammer/jwt-cpp.git
cd jwt-cpp/cmake
mkdir build
cd build
cmake ../../
make


g++ -std=c++11 -I. -Ijwt-cpp/include -o main -lcrypto -lcurl google_oidc.c

with env or on gce
export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/svc_account.json
./main https://foo.bar
   eyJhbGciOiJSUzI1NiIsImtpZ

with svc_json file
./main https://foo.bar `pwd`/svc_account.json

*/

using namespace std;

static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
    ((std::string *)userp)->append((char *)contents, size * nmemb);
    return size * nmemb;
}

bool verifyIdToken(std::string token, std::string audience, std::string certsReadBuffer)
{

    auto decoded_jwt = jwt::decode(token);
    picojson::value v;

    std::string err = picojson::parse(v, certsReadBuffer);
    if (!err.empty())
    {
        cerr << err << std::endl;
    }
    picojson::object obj = v.get<picojson::object>();

    // again, we can't use this JWK format of the endpoint
    // since the library i used here for JWK persing looks
    // for the x5c value (which doens't exist)  jwk.get_x5c_key_value();

    // auto jwks = jwt::parse_jwks(certsReadBuffer);
    // auto jwk = jwks.get_jwk(decoded_jwt.get_key_id());
    // auto issuer = decoded_jwt.get_issuer();
    //   jwk does not have an x5c claim so we can't do this stuff here:

    // auto x5c = jwk.get_x5c_key_value();
    // if (!x5c.empty() && !issuer.empty())
    // {
    //     auto verifier =
    //         jwt::verify()
    //             .allow_algorithm(jwt::algorithm::rs256(jwt::helper::convert_base64_der_to_pem(x5c), "", "", ""))
    //             .with_issuer("https://accounts.google.com")
    //             .leeway(60UL); // value in seconds, add some to compensate timeout

    //     verifier.verify(decoded_jwt);
    // }

    // so instead, we're using the PEM format endpoint "https://www.googleapis.com/oauth2/v1/certs";
    // parse that and try to verify using the provided key_id

    for (const auto e : obj)
    {
        // todo:  if the key_id isn't provided, then iterate over all keys and see
        //  if there's a match.  for now, i'm just expecting one to be there in the headers
        //  (which will be the case for google issued tokens)
        if (e.first == decoded_jwt.get_key_id())
        {
            auto verifier =
                jwt::verify()
                    .allow_algorithm(jwt::algorithm::rs256(e.second.get<string>(), "", "", ""))
                    .with_issuer("https://accounts.google.com")
                    .with_audience(audience)
                    .leeway(60UL); // value in seconds, add some to compensate timeout
            std::error_code ec;
            verifier.verify(decoded_jwt, ec);
            if (!ec)
            {
                std::cout << "id_token verified" << endl;
            }
            else
            {
                std::cout << "id_token verification Failed " << ec.message() << endl;
                return false;
            }
        }
    }
    return true;
}

int main(int argc, char *argv[])
{

    if (argc == 1 || argc > 3)
    {
        cerr << "usage: " << argv[0] << " <audience> <optional: /path/to/service_account.json>\n";
        exit(EXIT_FAILURE);
    }

    std::string target_audience = argv[1];

    CURL *curl;
    CURLcode res;
    std::string readBuffer;
    curl = curl_easy_init();

    std::string adc_file;

    if (argc == 3)
    {
        adc_file = argv[2];
    }
    else
    {
        char const *adc_env = std::getenv("GOOGLE_APPLICATION_CREDENTIALS");
        if (adc_env != NULL)
        {
            adc_file = std::string(adc_env);
        }
    }

    // download and cache the certs here if you want to
    // test the verification step.
    // google's jwk does not have an x5c claim so we can't use JWK endpoint
    // std::string url = "https://www.googleapis.com/oauth2/v3/certs";

    //  we have to instead use the PEM version here
    std::string url = "https://www.googleapis.com/oauth2/v1/certs";
    std::string certsReadBuffer;
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &certsReadBuffer);
    res = curl_easy_perform(curl);

    if (res != CURLE_OK)
    {
        std::cout << (stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        return -1;
    }

    // now generate the id_token
    if (!adc_file.empty())
    {
        // std::cout << "Using ADC File: " << adc_file << std::endl;

        std::ifstream adc_json;
        adc_json.open(adc_file);
        if (adc_json.fail())
        {
            cerr << "Error opening ADC file: " << strerror(errno);
        }
        // std::cout << adc_json.rdbuf();
        stringstream ss;
        ss << adc_json.rdbuf();

        picojson::value v;

        std::string err = picojson::parse(v, ss);
        if (!err.empty())
        {
            cerr << err << std::endl;
        }
        std::string type = v.get("type").get<string>();

        if (type == "service_account")
        {
            std::string issuer = v.get("client_email").get<string>();
            std::string audience = "https://oauth2.googleapis.com/token";

            std::string rsa_priv_key = v.get("private_key").get<string>();

            auto token = jwt::create()
                             .set_issuer(issuer)
                             .set_type("JWT")
                             .set_issued_at(std::chrono::system_clock::now())
                             .set_audience(audience)
                             .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{100})
                             .set_payload_claim("target_audience", jwt::claim(std::string{target_audience}))
                             .sign(jwt::algorithm::rs256("", rsa_priv_key, "", "notasecret"));

            std::string postData = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + token;

            curl_easy_setopt(curl, CURLOPT_URL, "https://oauth2.googleapis.com/token");
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
            curl_easy_setopt(curl, CURLOPT_POST, 1L);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());

            res = curl_easy_perform(curl);

            if (res != CURLE_OK)
            {
                std::cout << (stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
                return -1;
            }
            else
            {
                // std::cout << readBuffer << std::endl;
                picojson::value v;

                std::string err = picojson::parse(v, readBuffer);
                if (!err.empty())
                {
                std:
                    cerr << err << std::endl;
                }
                cout << v.get("id_token").get<string>().c_str() << endl;

                verifyIdToken(v.get("id_token").get<string>(), audience, certsReadBuffer);
            }
            curl_easy_cleanup(curl);
        }
        else if (type == "external_account")
        {
            cerr << "external_account not supported" << std::endl;
            // ref https://blog.salrashid.dev/articles/2022/workload_federation_cloudrun_gcf/
        }
        else
        {
            cerr << "Unknown credential file type  " << type << std::endl;
        }
    }
    else
    {
        // std::cout << "Using Metadata Server" << std::endl;
        std::string url = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" + target_audience + "&format=full";
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);

        struct curl_slist *list = NULL;
        list = curl_slist_append(list, "Metadata-Flavor: Google");
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);

        res = curl_easy_perform(curl);

        if (res != CURLE_OK)
        {
            std::cout << (stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
            return -1;
        }
        else
        {
            cout << readBuffer << endl;
            verifyIdToken(readBuffer, target_audience, certsReadBuffer);
        }
        curl_easy_cleanup(curl);
    }
}

How to verify an ID Token?

You can verify OIDC tokens manually if the inbound framework you deployed an application to does automatically perform the validation. In these cases, you the snippets provided above describe how to use google and other libraries to download the public certificates used to sign the JWT

Any validation should not just involve verifying the public certificate and validity but also that the audience claim matches the service being invoked. For more information, see Validating an ID Token. You can find samples here that implement validation.

This repo also includes various samples inline that verify tokens preferably by using google auth libraries (where applicable)

It is recommend to always verify locally but for debugging, you can use the tokenInfo endpoint or services that decode like jwt.io.

JWK Endpoints

The following lists out the JWK and OIDC endpoints for google, firebase and IAP


References

This site supports webmentions. Send me a mention via this form.