DEV Community

Saloni Kataria
Saloni Kataria

Posted on

3 Gotchas When Calling an IAP‑Protected Cloud Run API from a Chrome Extension (MV3)

This is a short, code‑first tutorial for developers. It assumes you already have a Cloud Run service and an OAuth2 Web Client. No business case study here—just the practical bits.

Summary

  • Use chrome.identity.launchWebAuthFlow to get a Google ID token.
  • Send it as Authorization: Bearer <token> to your IAP/IAM‑protected Cloud Run endpoint.
  • Verify the token audience on the server and cache tokens with a small expiry buffer on the client.

1) Getting an ID token (MV3)

background.js

async function getIdToken() { const redirectUrl = chrome.identity.getRedirectURL(); const u = new URL("https://accounts.google.com/o/oauth2/v2/auth"); u.searchParams.set("client_id", "YOUR_OAUTH_CLIENT_ID.apps.googleusercontent.com"); u.searchParams.set("response_type", "id_token"); u.searchParams.set("scope", "openid email profile"); u.searchParams.set("redirect_uri", redirectUrl); u.searchParams.set("nonce", String(Date.now())); u.searchParams.set("prompt", "select_account"); const { url } = await chrome.identity.launchWebAuthFlow({ url: u.toString(), interactive: true }); const params = new URLSearchParams(new URL(url).hash.substring(1)); const idToken = params.get("id_token"); if (!idToken) throw new Error("No id_token returned"); return idToken; } 
Enter fullscreen mode Exit fullscreen mode

Mini‑cache (optional):

function decodePayload(jwt){const b=jwt.split('.')[1].replace(/-/g,'+').replace(/_/g,'/');return JSON.parse(atob(b));} async function getCachedIdToken(){ const now=Math.floor(Date.now()/1000); const {token,exp}=await chrome.storage.local.get(["token","exp"]); if(token && exp && (exp-300)>now) return token; // 5‑min buffer const fresh=await getIdToken(); const {exp:e}=decodePayload(fresh); await chrome.storage.local.set({token:fresh, exp:e}); return fresh; } 
Enter fullscreen mode Exit fullscreen mode

2) Calling your API

popup.js

document.getElementById("go").addEventListener("click", async () => { const token = await chrome.runtime.sendMessage({ type: "GET_TOKEN" }); const res = await fetch("https://YOUR‑SERVICE‑HASH‑ue.a.run.app/run-secure-endpoint", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: (await chrome.tabs.query({active:true,currentWindow:true}))[0].url }) }); console.log(await res.text()); }); 
Enter fullscreen mode Exit fullscreen mode

And in background.js:

chrome.runtime.onMessage.addListener((m,_,send)=>{ if(m?.type==="GET_TOKEN") getCachedIdToken().then(t=>send(t)).catch(e=>send({error:String(e)})); return true; }); 
Enter fullscreen mode Exit fullscreen mode

3) Verifying on Flask (Cloud Run)

main.py

from flask import Flask, request, jsonify from google.oauth2 import id_token from google.auth.transport import requests as greq import os app = Flask(__name__) IAP_AUDIENCE = os.environ.get("IAP_OAUTH_CLIENT_ID","") # set this for IAP  def verify_google_id_token(tok:str)->dict: return id_token.verify_oauth2_token(tok, greq.Request(), audience=IAP_AUDIENCE) @app.post("/run-secure-endpoint") def run_secure(): auth = request.headers.get("Authorization","") if not auth.startswith("Bearer "): return jsonify({"error":"missing bearer"}), 401 try: payload = verify_google_id_token(auth.split()[1]) except Exception as e: return jsonify({"error":"invalid token","detail":str(e)}), 401 return jsonify({"ok": True, "email": payload.get("email")}) 
Enter fullscreen mode Exit fullscreen mode

Common gotchas (and fixes)

  • 401 with IAP: wrong audience. Use the IAP OAuth Client ID as aud when verifying.
  • Empty id_token: check your OAuth2 client type (Web) and the redirect URI from getRedirectURL().
  • Tokens expiring mid‑session: cache with a small buffer (5 minutes) and refresh on demand.
  • CORS: add Access-Control-Allow-Origin + Authorization header allowances if you call from content scripts or pages.

Minimal repo layout

repo/ ├─ extension/ (manifest.json, background.js, popup.{html,js}) └─ backend/ (main.py, requirements.txt, Dockerfile) 
Enter fullscreen mode Exit fullscreen mode

That’s it—short and sweet. If you need a deeper walkthrough, look for my longer guide or case‑study later.

Top comments (1)

Collapse
 
om_shree_0709 profile image
Om Shree

Nice Article Sir!