Secure Federated Access to Google Cloud: Building a Mock OIDC Identity Provider

Modern cloud environments are rarely isolated. Applications often span across multiple clouds or include on-premise components. Some organisations operate in a hybrid mode by design, others are mid-migration, while others may have regulatory or compliance reasons for retaining systems outside public cloud environments (i.e. on-premises). In my client engagements, I frequently see scenarios where applications running on-premise or in other clouds (like AWS or Azure) need to securely access Google Cloud resources.

Too often, organisations download and distribute long-lived service account keys to address the authentication requirements. These are usually integrated into applications, Python scripts, or Continuous Integration (CI) (e.g. Jenkins) pipelines. This approach introduces significant security risks - if the key is compromised, it can be used to impersonate the service account without detection, and access sensitive resources - and should be highly discouraged. In fact, Google Cloud offers multiple best practice guides for managing service accounts and service account keys (which should be avoided entirely), yet not all organisations follows good IAM hygiene. That's where Workload Identity Federation comes in.

Image credit: Gemini

Workload Identity Federation (WIF) lets you use Identity and Access Management (IAM) to grant direct access on Google Cloud resources to external identities. It eliminates the maintenance and security burden associated with service account keys, and instead grants temporary, scoped access using identity tokens from external OpenID Connect (OIDC) providers. The integration paths for AWS and Azure are well-documented, but my clients often get stuck when setting up and testing the flows for on-premise or custom OIDC identity providers.

To address this, I built a mock OIDC Identity Provider (IdP), designed specifically for simulating federated access to Google Cloud. This is a lightweight, Flask-based programmable server that issues signed ID tokens and mimics partial OIDC behaviour. Needless to say, this is meant for testing only, not for production use. To illustrate its usage, in the first post of a two-part series, I'll show how to build and deploy the mock OIDC identity provider. In the next post, I'll walk through the end-to-end flow for federated identity access to Google Cloud using a headless, Streamlit-based OIDC client.

What is OpenID Connect (OIDC?)

OpenID Connect (OIDC) is a modern identity protocol built on top of OAuth 2.0, designed to address its shortcomings around authentication. OIDC allows clients - applications, scripts, services - to verify the identity of users or workloads, based on authentication performed by an external Identity Provider (IdP). The result is an ID token, a signed JSON Web Token (JWT) that contains a set of claims about the authenticated entity. In the context of Google Cloud, OIDC provides a standard, secure way to federate identity across trust boundaries, without storing or distributing long-lived credentials. Workload Identity Federation builds on this, enabling short-lived access using the ID tokens. While this post focuses on the mock OIDC IdP, I explain the OIDC authentication flows in more detail here.

Building the Mock OIDC Identity Provider

While the mock OIDC IdP does not strictly adhere to the OIDC specifications and implement all the necessary endpoints, it offers enough to test Google Cloud WIF integration using a headless clients. The mock IdP supports:

  • Serving a valid OIDC discovery document
  • Hosting a JWKS endpoint so Google (or any client) can verify issued tokens
  • Issuing signed ID tokens with a customisable subject, email, and role
  • Stateless deployment — you can run it in a container anywhere e.g Cloud RunRailway, or DigitalOcean

The OIDC IdP service is a simple Flask app that signs ID tokens using a provided RSA private key. It exposes a few environment variables to configure behaviour:

VariableDescription
PRIVATE_KEY_PEMThe RSA private key in PEM format, used to sign tokens
ISSUER_URLThe OIDC issuer URL (must match what clients expect)
AUDIENCEThe audience (aud) claim in the token, often tied to a specific provider
KEY_IDThe key ID (kid) used in JWT headers and JWKS metadata

All tokens issued follow the OIDC specification and include standard claims like iss, sub, aud, iat, exp, and email. You can generate them using a simple REST call to the /generate-token endpoint.

You can generate the RSA key pair using standard OpenSSL libraries. Make sure you store the private key in a secure location.

openssl genrsa -out private_key.pem 2048

openssl rsa -in private_key.pem -pubout -out public_key.pem

The OIDC IdP exposes the following endpoints:

EndpointDescription
/Health check
/.well-known/openid-configurationStandard OIDC discovery document
/jwks.jsonReturns the public RSA key in JWKS format
/generate-tokenIssues a signed OIDC ID token
/authorizePresent for compatibility, returns 501 (not implemented)

The /generate-token endpoint accepts both GET and POST requests and supports attributes like sub, email, and role, letting you simulate different workloads. For this walk through, only the sub and email attributes will be used.

Deploying the Mock OIDC Identity Provider to Cloud Run

To make the mock OIDC IdP accessible at a stable URL over HTTPS, let's deploy it to Cloud Run, Google's fully managed container hosting platform. If you are using Google Cloud Shell, clone the GitHub repository, and change to the mock-oidc-idp directory. Make sure you select the project and authenticate yourself.

gcloud auth login
gcloud config set project YOUR_PROJECT_ID

Use Secret Manager to create the secrets described in the section above. You can then create environment variables and reference these secrets - Cloud Run will automatically populate the latest version at runtime.

  • PRIVATE_KEY_PEM
  • ISSUER_URL
  • AUDIENCE
  • KEY_ID
💡
ISSUER_URL refers to the actual URL that Cloud Run assigns after deployment, or your custom domain mapping if you choose to do so. This will not be available during the first deployment; you'll need to update the secret with the Cloud Run URL and redeploy the service.

AUDIENCE refers to the workload identity pool provider full resource name that we're yet to set up; I'll cover this in my next post. It should be something like: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID

For ease of deployment, I have created the following Dockerfile. Create a new Cloud Run service, select Continuously deploy from a repository, and set up the remote repository with Cloud Build. Select Dockerfile, provide the source location, update container properties if necessary, and bind the previously created secrets as environment variables. Once deployed, obtain the Cloud Run URL - either the built-in one, or after custom domain mapping, update the ISSUER_URL secret, and redeploy the service. If all goes well, your mock OIDC IdP should now be ready.

# Use the official lightweight Python image.
FROM python:3.12-slim

# Allow statements and log messages to immediately appear in the logs
ENV PYTHONUNBUFFERED True

# Copy local code to the container image
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Set the default port
ENV PORT=8080

# Run the web service on container startup.
CMD gunicorn --bind 0.0.0.0:"${PORT}" app:app

Testing the OIDC IdP Endpoints

You can test that the OIDC discovery works by querying the .well-known endpoint i.e. https://mock-oidc-idp.your-domain.com/.well-known/openid-configuration. You should receive a valid JSON document with your issuer and JWKS URI.

{
  "issuer": "https://mock-oidc-idp.your-domain.com",
  "jwks_uri": "https://mock-oidc-idp.your-domain.com/jwks.json",
  "response_types_supported": [
    "id_token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ]
}

You can also retrieve the JWT public key set at https://mock-oidc-idp.your-domain.com/jwks.json.

{
  "keys": [
    {
      "alg": "RS256",
      "e": "AQAB",
      "kid": "mock-oidc-idp-key-20250708",
      "kty": "RSA",
      "n": "oaJVUI1V5o-76U905Qvlmmu3_VUpHxyYD_pb11h7HqePyqOl4TIw53zMMEakiPMEh-oQchDyEiGHZtClxd6lG5t2SWXjiF7gh0bLeKCq101nqdYOAlN_Wn7HpHQa7uExSUrImQNh2-CZiQ7lK1H0jwaVWfGBmtVaxCFr818dnV5Fd6gpykQdnDpS60_vpw4kDLZfUfuwAbRvh27kL72WSljINJMksRFoncNUGFswBPwxFu8s3kpfD_L0_QNeIDhzwxV9_qV6QsAMH8gIgkRTTIKbapKlejuQf_0dvw9vGwnzZnVF59I1ZEZBXS23O--4Gw4gY0YKKcVMaDMEDjF-ew",
      "use": "sig"
    }
  ]
}

To test token generation, use the following request:

curl -X POST https://<your-oidc-idp-url>/generate-token \
  -H "Content-Type: application/json" \
  -d '{
        "sub": "gcs-sa", 
        "email": "gcs-sa@your-project.iam.gserviceaccount.com
        "role": "admin"
        }'

The response will include a signed ID token, which you can use with Google Cloud services via federation flows:

{
  "id_token": "<signed JWT>",
  "subject": "gcs-sa"
}

PS: You can use jwt.io to decode, validate, and verify the signed JWT token.

Decoded JWT sample using jwt.io

Security Considerations

This mock OIDC Identity Provider is designed solely for development and testing purposes. It issues signed ID tokens without any authentication or authorisation checks, and uses static RSA keys loaded from secrets. Despite its simplicity, it’s a powerful tool for experimenting with federated identity flows — especially in scenarios where setting up a full-featured IdP would be an overkill. It’s been invaluable in my client work, and I hope it helps others as well. In the next post, we’ll see this IdP in action as we configure federated access to Google Cloud using workload Identity Federation.