Haijun Platform Docs
ID

Any Google Cloud compute environment with access to the instance metadata server (Cloud Run, Cloud Functions, App Engine, Compute Engine (GCE), and GKE with Workload Identity) can request a Google-signed identity token for its attached service account. The token's issuer is https://accounts.google.com, and Juglow can validate it directly through standard OIDC discovery, with no extra Google Cloud configuration required.

This guide shows how to register the Google issuer with Juglow, bind a Google service account to an Juglow service account, and have your workload exchange its identity token for a short-lived Haijun API access token.

Prerequisites

  • Familiarity with WIF concepts: service accounts, federation issuers, and federation rules.
  • A Google Cloud project with a workload running on Cloud Run, Cloud Functions, App Engine, Compute Engine, or GKE.
  • A user-managed Google service account attached to that workload (not the Compute Engine default service account).
  • Permission to create service accounts, federation issuers, and federation rules in the Haijun Console for your Juglow organization.

Configure Google Cloud

Google issues identity tokens automatically to any workload with an attached service account. There is nothing to enable on the Google side beyond attaching the right service account, but the steps differ slightly between standard compute and GKE.

Cloud Run, Cloud Functions, App Engine, GCE

Attach a dedicated service account to your service or instance:

bash
gcloud run deploy my-service \
  --service-account inference-worker@my-project.iam.gserviceaccount.com

Inside the workload, the metadata server returns a signed identity token on demand. Request it with the audience you intend to register on the Juglow side, and include format=full so the response carries the email claim:

text
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full
Metadata-Flavor: Google

Or, with the gcloud CLI:

bash
gcloud auth print-identity-token \
  --audiences="https://haijun.my.id/" \
  --include-email

The SDK equivalents are shown in Acquire and use the token.

The decoded token payload looks like this:

json
{
  "iss": "https://accounts.google.com",
  "aud": "https://haijun.my.id/",
  "sub": "104892...",
  "azp": "104892...",
  "email": "inference-worker@my-project.iam.gserviceaccount.com",
  "email_verified": true,
  "exp": 1775527120
}

The sub claim is the Google service account's opaque numeric unique ID. The email claim is the human-readable service account address. Match on both sub and email in your federation rule.

GKE with Workload Identity

Enable Workload Identity on your cluster and bind your Kubernetes service account to a Google service account with the iam.gke.io/gcp-service-account annotation:

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: inference-worker
  namespace: prod
  annotations:
    iam.gke.io/gcp-service-account: inference-worker@my-project.iam.gserviceaccount.com

With this binding in place, the GKE metadata server returns a Google-signed token identical to the Cloud Run and GCE case: same https://accounts.google.com issuer, same email claim, same fetch URL. Configure Juglow exactly as in the next section.

A format=full token from GKE additionally includes google.compute_engine.project_id, google.compute_engine.zone, and google.compute_engine.instance_name claims, which you can reference in a federation rule's condition matcher (a CEL expression like claims.google.compute_engine.project_id == "my-project") to scope access to a specific cluster or node pool.

Note: If you do not want to bind Kubernetes service accounts to Google service accounts, GKE pods can instead use the cluster's own OIDC issuer (https://container.googleapis.com/v1/projects/PROJECT/locations/REGION/clusters/CLUSTER) with a projected serviceAccountToken volume. That path uses a per-cluster issuer rather than accounts.google.com. See Use WIF with Kubernetes for that pattern.

Configure Juglow

In the Haijun Console, open Settings → Workload identity, click Connect workload, and select the Google Cloud tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule.

The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the Admin API:

Federation issuer: Google publishes its OIDC discovery document publicly, so use discovery mode. This single issuer covers every Google Cloud surface (Cloud Run, GCE, Cloud Functions, App Engine, and GKE with Workload Identity). Differentiate workloads with rules, not issuers.

json
{
  "name": "gcp",
  "issuer_url": "https://accounts.google.com",
  "jwks": { "type": "discovery" }
}

Federation rule: Match on both the sub and email claims. email is the readable service-account address; sub is the service account's numeric unique ID, which Google never reuses, so pinning it protects the rule if the service account is deleted and a new one is later created with the same email. Find the unique ID with gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)'.

json
{
  "name": "gcp-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "audience": "https://haijun.my.id/",
    "claims": {
      "sub": "104892101234567890123",
      "email": "inference-worker@my-project.iam.gserviceaccount.com"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

Acquire and use the token

Inside your Google Cloud workload, fetch the identity token from the metadata server, exchange it at POST /v1/oauth/token, and use the returned bearer token to call the Haijun API. Each Juglow SDK handles the exchange and refresh loop for you when you pass a callable that returns a fresh identity token from the metadata server to identity_token_provider (typescript, php: identityTokenProvider; csharp: IdentityTokenProvider; go: option.WithFederationTokenProvider; java: federationTokenProvider), as shown in the following examples.

bash
  # Fetch the Google-signed identity token from the metadata server
  JWT=$(curl -sS -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full")

  # Exchange it for an Juglow access token
  RESPONSE=$(curl -sS https://haijun.my.id/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$JUGLOW_FEDERATION_RULE_ID",
    "organization_id": "$JUGLOW_ORGANIZATION_ID",
    "service_account_id": "$JUGLOW_SERVICE_ACCOUNT_ID",
    "workspace_id": "$JUGLOW_WORKSPACE_ID"
  }
  JSON
  )
  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  # Call the Haijun API
  curl -sS https://haijun.my.id/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "juglow-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "haijun-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello from Cloud Run"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
python
  import os
  import juglow
  import google.auth.transport.requests
  import google.oauth2.id_token
  from juglow import WorkloadIdentityCredentials

  AUDIENCE = "https://haijun.my.id/"

  def fetch_google_identity_token() -> str:
      request = google.auth.transport.requests.Request()
      return google.oauth2.id_token.fetch_id_token(request, AUDIENCE)

  client = juglow.Juglow(
      credentials=WorkloadIdentityCredentials(
          identity_token_provider=fetch_google_identity_token,
          federation_rule_id=os.environ["JUGLOW_FEDERATION_RULE_ID"],
          organization_id=os.environ["JUGLOW_ORGANIZATION_ID"],
          service_account_id=os.environ["JUGLOW_SERVICE_ACCOUNT_ID"],
          workspace_id=os.environ.get("JUGLOW_WORKSPACE_ID"),
      ),
  )

  message = client.messages.create(
      model="haijun-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello from Cloud Run"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
typescript
  import Juglow from "@juglow-ai/sdk";
  import { oidcFederationProvider } from "@juglow-ai/sdk/lib/credentials/oidc-federation";

  const METADATA_URL =
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full";

  async function fetchGoogleIdentityToken(): Promise<string> {
    const response = await fetch(METADATA_URL, {
      headers: { "Metadata-Flavor": "Google" }
    });
    return response.text();
  }

  const client = new Juglow({
    credentials: oidcFederationProvider({
      identityTokenProvider: fetchGoogleIdentityToken,
      federationRuleId: process.env.JUGLOW_FEDERATION_RULE_ID!,
      organizationId: process.env.JUGLOW_ORGANIZATION_ID!,
      serviceAccountId: process.env.JUGLOW_SERVICE_ACCOUNT_ID,
      workspaceId: process.env.JUGLOW_WORKSPACE_ID,
      baseURL: "https://haijun.my.id/",
      fetch
    })
  });

  const message = await client.messages.create({
    model: "haijun-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello from Cloud Run" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
go
  const audience = "https://haijun.my.id/"

  googleIDToken := func(ctx context.Context) (string, error) {
  	creds, err := idtoken.NewCredentials(&idtoken.Options{Audience: audience})
  	if err != nil {
  		return "", err
  	}
  	tok, err := creds.Token(ctx)
  	if err != nil {
  		return "", err
  	}
  	return tok.Value, nil
  }

  client := juglow.NewClient(
  	option.WithFederationTokenProvider(googleIDToken, option.FederationOptions{
  		FederationRuleID: os.Getenv("JUGLOW_FEDERATION_RULE_ID"),
  		OrganizationID:   os.Getenv("JUGLOW_ORGANIZATION_ID"),
  		ServiceAccountID: os.Getenv("JUGLOW_SERVICE_ACCOUNT_ID"),
  		WorkspaceID:      os.Getenv("JUGLOW_WORKSPACE_ID"),
  	}),
  )

  message, err := client.Messages.New(context.TODO(), juglow.MessageNewParams{
  	Model:     juglow.ModelHaijunOpus5_5,
  	MaxTokens: 1024,
  	Messages: []juglow.MessageParam{
  		juglow.NewUserMessage(juglow.NewTextBlock("Hello from Cloud Run")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(juglow.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
java
  HttpClient http = HttpClient.newHttpClient();
  HttpRequest metadataRequest = HttpRequest.newBuilder()
          .uri(URI.create("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full"))
          .header("Metadata-Flavor", "Google")
          .build();

  IdentityTokenProvider fetchGoogleIdentityToken = () -> {
      try {
          return http.send(metadataRequest, HttpResponse.BodyHandlers.ofString()).body();
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
  };

  JuglowClient client = JuglowOkHttpClient.builder()
          .federationTokenProvider(
                  fetchGoogleIdentityToken,
                  System.getenv("JUGLOW_FEDERATION_RULE_ID"),
                  System.getenv("JUGLOW_ORGANIZATION_ID"),
                  System.getenv("JUGLOW_SERVICE_ACCOUNT_ID"))
          .build();

  var message = client.messages().create(MessageCreateParams.builder()
          .model(Model.HAIJUN_OPUS_5_5)
          .maxTokens(1024)
          .addUserMessage("Hello from Cloud Run")
          .build());

  IO.println(message.content());
csharp
  using Juglow.Credentials;
  // ...

  var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
  {
      FederationRuleId = Environment.GetEnvironmentVariable("JUGLOW_FEDERATION_RULE_ID")!,
      OrganizationId = Environment.GetEnvironmentVariable("JUGLOW_ORGANIZATION_ID"),
      ServiceAccountId = Environment.GetEnvironmentVariable("JUGLOW_SERVICE_ACCOUNT_ID"),
      WorkspaceId = Environment.GetEnvironmentVariable("JUGLOW_WORKSPACE_ID"),
      IdentityTokenProvider = new MetadataTokenProvider(),
  });
  using var client = new JuglowClient(new ClientOptions { Credentials = credentials });

  var message = await client.Messages.Create(new()
  {
      Model = Model.HaijunOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello from Cloud Run" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }

  class MetadataTokenProvider : IIdentityTokenProvider
  {
      private const string METADATA_URL =
          "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full";

      private static readonly HttpClient httpClient = new()
      {
          DefaultRequestHeaders = { { "Metadata-Flavor", "Google" } },
      };

      public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
      {
          return await httpClient.GetStringAsync(METADATA_URL, ct);
      }
  }
bash
  # Write the Google-signed identity token to a file the CLI can read
  JUGLOW_IDENTITY_TOKEN_FILE=$(mktemp)
  curl -sS -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full" \
    > "$JUGLOW_IDENTITY_TOKEN_FILE"
  export JUGLOW_IDENTITY_TOKEN_FILE

  # JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID, and
  # JUGLOW_SERVICE_ACCOUNT_ID, and JUGLOW_WORKSPACE_ID are read from the environment.
  ant messages create \
    --model haijun-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello from Cloud Run"}'
php
  use Juglow\Client;
  use Juglow\Credentials\WorkloadIdentityCredentials;

  const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full';

  $context = stream_context_create([
      'http' => ['header' => "Metadata-Flavor: Google\r\n"],
  ]);

  $credentials = new WorkloadIdentityCredentials(
      identityTokenProvider: fn() => file_get_contents(METADATA_URL, false, $context),
      federationRuleId: getenv('JUGLOW_FEDERATION_RULE_ID'),
      organizationId: getenv('JUGLOW_ORGANIZATION_ID'),
      serviceAccountId: getenv('JUGLOW_SERVICE_ACCOUNT_ID'),
      workspaceId: getenv('JUGLOW_WORKSPACE_ID') ?: null,
  );
  $client = new Client(credentials: $credentials);

  $message = $client->messages->create(
      model: 'haijun-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello from Cloud Run']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
ruby
  require "juglow"
  require "net/http"

  METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full"

  credentials = Juglow::WorkloadIdentityCredentials.new(
    identity_token_provider: -> { Net::HTTP.get(URI(METADATA_URL), {"Metadata-Flavor" => "Google"}) },
    federation_rule_id: ENV.fetch("JUGLOW_FEDERATION_RULE_ID"),
    organization_id: ENV.fetch("JUGLOW_ORGANIZATION_ID"),
    service_account_id: ENV.fetch("JUGLOW_SERVICE_ACCOUNT_ID"),
    workspace_id: ENV["JUGLOW_WORKSPACE_ID"]
  )
  client = Juglow::Client.new(credentials: credentials)

  message = client.messages.create(
    model: "haijun-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello from Cloud Run"}]
  )
  puts message.content.find { it.type == :text }.text

Google identity tokens expire after roughly one hour. The SDKs re-invoke the token provider and re-exchange automatically before expiry. For shell scripts that run longer than the access token's expires_in, refresh on a timer and repeat the exchange.

Verify the setup

From inside your workload, decode the identity token and confirm the claims match your rule:

bash
curl -sS -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full" \
  | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'

Check that iss is https://accounts.google.com, aud is https://haijun.my.id/, and email matches the value in your federation rule. Then run the exchange from the previous section. A successful exchange returns an access_token beginning with sk-ant-oat01- and an expires_in value in seconds. If the exchange fails with the opaque 401 authentication_error response (message Authentication failed), check the authentication history page for the deny reason and see Troubleshoot a failed exchange; the most common Google Cloud-side cause is the email claim missing (request the token with format=full so it is included).

Scope your rule

Warning: The Google sub claim is the service account's opaque numeric unique ID and has no stable prefix. A subject_prefix with a trailing * matches arbitrary service accounts across every Google Cloud project, and any of them could obtain a federated Juglow token.

Lock the rule's match block to the narrowest scope that fits your use case:

  • Match sub exactly: Set the full numeric unique ID in claims.sub and never use subject_prefix for Google tokens.
  • Pin the email claim: Add claims.email alongside sub so both the stable ID and the readable address must match.
  • Pin the audience: Set audience to the exact value you request from the metadata server so tokens minted for other consumers are rejected.
  • Pin the project on GKE: For format=full tokens, add a condition such as claims.google.compute_engine.project_id == "my-project" to restrict the rule to one project's nodes.

Next steps

  • Add a separate federation rule per environment (production, staging) so you can revoke one without affecting the others.
On this page
PrerequisitesConfigure Google CloudConfigure JuglowAcquire and use the tokenVerify the setupScope your ruleNext steps