I’m currently working on several cloud-native projects hosted on Google Cloud Platform (GCP) that use .NET for the API and React for the UI. These projects rely on GCP’s Identity-Aware Proxy (IAP) to handle authentication, which occurs before any requests reach the Application Load Balancer or the application itself.
While GCP’s IAP offers robust security benefits, configuring a .NET and React application to work seamlessly with it—both locally during development and when deployed to a Cloud Run instance as a Docker container—proved to be more challenging than I expected. The available documentation and resources for this setup are sparse and often fragmented, making it difficult to piece together a clear solution.
This blog post is my way of sharing the lessons I’ve learned along the way. By outlining the steps and solutions that worked for me, I hope to save others from the trial and error I experienced and provide a clearer path for integrating GCP IAP with .NET and React applications.
Architecture
A small to medium-sized application deployed to GCP would be containerized and run on Cloud Run and fronted by an Application Load Balancer. The Application Load Balancer handles several tasks: providing an HTTPS frontend, performing path-based routing, and delegating user authentication to IAP.
For instance, if a user visits https://myapp.com, the ALB receives the request. IAP checks whether the user is authenticated. If not, IAP starts an OAuth2 workflow, redirecting the user to a federated identity provider (e.g., Google or Okta) to log in. Once the user successfully logs in, they are redirected back to the ALB, which routes the traffic to the correct Cloud Run container based on the URL path.
A visualization of this architecture is as follows:

What GCP IAP Is Actually Doing
After the federated authentication has completed, IAP then does a Set-Cookie
with an auth token. This token allows the browser to include the cookie in subsequent requests to the Application Load Balancer, and IAP uses it to validate that the user is still authenticated. Once validated, IAP forwards the relevant authentication information to the appropriate Cloud Run instances. The name of the cookie changes, but it is always prefixed by __Host-GCP_IAP_AUTH_TOKEN_
.
One challenge for local development is the way the cookie is configured. It is set with HttpOnly
(meaning it cannot be accessed via JS) and SameSite=none
(meaning the browser will only include the cookie if the request originates from the same domain and is in a top-level frame). Later, I’ll show how these two implications come into play.
Deployed UI Auth
The easiest part of setting up this kind of application was getting the UI to run on Cloud Run. Since requests to the UI are handled by the Application Load Balancer, which manages authentication through IAP, I know that if a request hits the UI Docker container and the UI is actually running in the browser, then the user has been authenticated. This eliminates much of the complexity involved in securing the application in the deployed environment.
This setup is a significant improvement over our previous experience on Azure with MSAL, where the UI had to handle its own authentication flow, manage a bearer token, and deal with related challenges. With GCP IAP, when the JavaScript runs on https://myapp.com
and makes an AJAX request to https://myapp.com/api/SomeAPI
, the browser automatically includes the GCP IAP authentication cookie in the request. This ensures seamless and consistent authentication for all interactions.
Deployed API Auth
There wasn’t a ton of documentation around deploying API auth, so I had to piece a lot of it together by looking at various pieces of documentation (of varying quality) to actually figure out how it works. As mentioned before, the client sends a GCP IAP token as a cookie to the Application Load Balancer, which validates the token through IAP and allows the request to proceed. The request is then routed to the API Cloud Run – with some interesting HTTP headers. I figured this out after looking at this document. The headers it sends are:
x-goog-authenticated-user-id: accounts.google.com:1234567890 x-goog-authenticated-user-email: accounts.google.com:[email protected] x-goog-iap-jwt-assertion: (a JWT bearer token) x-serverless-authorization: (an ID token used by IAP to authenticate to Cloud Run)
The JWT bearer token that’s being passed along in the request is the one I’m interested in. It appears to be an ID token and includes the user’s email address. Since my application already supports JWT bearer token authorization through the Authorization header, it was very minor to get my application to recognize this.
There are a few different ways to actually implement this with MSAL. I chose to add middleware that retrieves the x-goog-iap-jwt-assertion
header and assigns its value to the Authorization
header. I then updated the audience and issuer settings in my TokenValidationParameters
. The difficult part was figuring out how to validate the token’s signature. Using the documentation mentioned earlier, I found a URL to retrieve the JWKs. To generate the TokenValidationParameters
, I fetched the JWKs from that URL, converted them into signing keys, and passed them along as the IssuerSigningKeys
.
var jsonWebKeySetJSON = response.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); var jsonWebKeySet = new JsonWebKeySet(jsonWebKeySetJSON); var signingKeys = jsonWebKeySet.GetSigningKeys();
Under the covers, this appears to be what the libraries for integrating with GCP IAP are already doing. However, because of the way our application is set up, I had to manually wire some of this into the MSAL middleware. In other languages or setups, this process would likely be much simpler.
Once I got that working, I could use the ClaimsIdentity
built into the HttpContext
to get the user’s email address. And because it came from a validated JWT that I know came from GCP IAP as the issuer, I’m able to then enforce authorization in my application.
Local API Auth
For local development, I wanted the API to follow the same authorization and authentication pipeline as the deployed API. However, this ended up being tricky because my local API isn’t fronted by the same Application Load Balancer and IAP pair. As a result, I couldn’t use the authentication cookie available in my browser after completing the deployed authentication workflow.
The only way I was able to get this to work was to leverage the bearer token forwarded to Cloud Run as a header. I created a local endpoint, restricted to users with the Administrator (developer) role, that returns the bearer token. This allows me to reuse the same bearer token for local development, changing the audience and issuer configuration to match.
Local UI Auth
The absolute hardest part of this entire workflow was enabling the local UI development to run through the same authentication and authorization pipeline as the deployed application. Since the IAP cookie has SameSite=none
, any requests made from the local development UI server won’t include the IAP access token cookie. Overall, this is a good thing; it’s a safety and security issue that Chrome and other browsers have begun enforcing. However, it’s bad news for our local UI development.
The way to lean into this approach is to accept that the cookie will only be set on the deployed application’s domain and only when the JavaScript making the AJAX request is executing in the top-level frame of the browser window. We can then use a local reverse proxy, like Fiddler, to make the local UI application appear as though it’s part of the deployed application. This still allows requests to the API to pass through to the deployed API.
For instance, suppose my API is hosted at https://myapp.com/api/
. All traffic matching the /api/
path is routed by the Application Load Balancer to the API Cloud Run instance, while other traffic is sent to the UI Cloud Run instance. With Fiddler, I can mimic this setup by routing API traffic to the deployed API while directing static traffic to my local UI development environment:
The /api/
rule takes precedence because it’s first and more specific, so if it doesn’t match, the traffic falls through to a default rule. This setup allows the local UI to function as though it were a deployed UI, with one crucial limitation: the local UI is not fronted by the Application Load Balancer/IAP pair. As a result, I can’t guarantee that the correct cookie has been set for the local UI to pass along to the deployed API.
To handle this, we can leverage the fact that if the cookie is missing or invalid, the API will return a 401 Unauthorized
response. To account for this in the UI’s authorization logic, the application can first make an AJAX call to an endpoint like https://myapp.com/api/authentication/GetUserInformation
. This endpoint retrieves the logged-in user’s name, email address, and roles from the bearer token forwarded by IAP. If this call returns a non-200 response, it indicates that the cookie is missing or incorrect.
At this point, the UI can check if the document.location.hostname
is localhost
. If it is, the UI recognizes that it is running locally and interacting with the deployed API. The UI can then redirect the user to an OAuth endpoint, which authenticates the user and redirects them back to the local UI after authentication. This process ensures that the IAP cookie is properly set, allowing subsequent API calls to include the cookie and return the necessary user information.
Concluding Thoughts
Once I was able to get everything working, I honestly liked GCP’s IAP-based authentication workflow to more than Microsoft Azure AD (now Entra). Especially on the UI side, the security enforcement feels streamlined since the browser handles bearer token management automatically via a HttpOnly
and SameSite=none
cookie.
That said, the development experience presented significant challenges. Where I landed was the result of about a week’s worth of trial and error; the UI definitely took us the longest to get working. While the end result is secure and efficient, the journey to get there highlighted areas where GCP’s documentation and tooling could better support developers during local development.
If you’re navigating similar challenges on your path to building cloud-native applications with GCP, I hope this guide serves as a helpful reference. By sharing what I learned, I aim to reduce some of the trial and error for others, empowering teams to implement secure, scalable, and efficient solutions with greater confidence.
If you found this deep dive into GCP IAP integration helpful, be sure to check out the other articles on the Keyhole Software Dev Blog. We cover a wide range of topics, from cloud-native development and microservices to cutting-edge frameworks like React and .NET Core. Whether you’re tackling tricky authentication workflows, optimizing application performance, or exploring new tools in the tech landscape, our blog is packed with insights, tutorials, and real-world experiences from our team of expert consultants. Visit us at Keyhole Dev Blog to keep learning and growing!