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!