[FR] Workflow Identity Federation

Note: AI helped to formulate the text but the content is 100% curated by a human

Feature Request: Workload Identity Federation for Foundry Multipass


1. Problem Statement

Today, any automated workload that needs to call Foundry APIs must hold a long-lived credential: either a personal access token or a TPA client_id + client_secret. These credentials must be stored somewhere - a Foundry dataset, a Foundry Source, a secrets manager, an environment variable - and they carry serious operational risks:

  • Blast radius on leak. A stolen client_secret grants the thief all scopes the TPA holds, indefinitely, until manually rotated.
  • Rotation burden. Secrets must be rotated on a schedule and pushed to every consuming workload. Any lag creates a credential-validity gap. Foundry TPAs only support one active client_secret at a time, making zero-downtime rotation impossible: the old secret must be invalidated before the new one is distributed, guaranteeing a window of failed authentications.
  • Intra-Foundry anti-pattern. When a Foundry Transform or Function needs to call Foundry’s own APIs under a specific scope, there is no clean mechanism. The current workaround is to materialise a client_secret inside a Foundry Source or dataset - a practice that security teams universally flag.

Every major data platform adjacent to Foundry - Snowflake, Databricks, AWS itself - now supports Workload Identity Federation (WIF): short-lived OIDC tokens issued by a cloud provider’s native identity system are exchanged for a scoped, time-limited access token at the target platform.


2. Prior Art

2.1 AWS - sts:GetWebIdentityToken

An IAM principal calls:

aws sts get-web-identity-token --audience <audience> --signing-algorithm ES384

The returned JWT has the structure:

{
  "aud": "https://mystack.palantirfoundry.com",
  "sub": "arn:aws:iam::123456789012:role/my-pipeline-role",
  "iss": "https://xxx.tokens.sts.global.api.aws",
  "iat": 1765140480,
  "exp": 1765140780,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "https://sts.amazonaws.com/": {
    "org_id": "o-exampleorgid11",
    "aws_account": "123456789012",
    "ou_path": ["ou-root-example1"],
    "request_tags": {
      "scopes": "api:datasets-read api:ontologies-read",
      "resources": "ri.compass.main.folder.0b6ae60f-468d-411d-8d71-ee9f18810672"
    },
    "original_session_exp": "2025-12-07T22:17:32Z",
    "source_region": "eu-central-1",
    "principal_id": "arn:aws:iam::123456789012:role/my-pipeline-role",
    "identity_store_user_id": "9067c4f2-d0a2-70c0-8fa8-example"
  }
}

Tags passed directly to get-web-identity-token (up to 50) are embedded in the JWT under the https://sts.amazonaws.com/ namespace as request_tags. Multipass reads request_tags.scopes and request_tags.resources directly from the validated JWT.

2.2 Snowflake WIF

Snowflake maps an IAM Role ARN to a SERVICE user via CREATE USER ... WORKLOAD_IDENTITY:

CREATE USER my_pipeline
  TYPE = SERVICE
  WORKLOAD_IDENTITY = (
    ISSUER = 'https://sts.amazonaws.com/accounts/123456789012'
    SUBJECT = 'arn:aws:iam::123456789012:role/my-pipeline-role'
  );

The connector presents the JWT to Snowflake’s token endpoint; Snowflake validates the JWT signature against the issuer’s JWKS and issues a session token. The Python connector (v4.5.0+) handles the exchange transparently when authenticator=workload_identity.

2.3 Databricks OAuth Token Federation

Databricks exposes a standard RFC 8693 token exchange endpoint:

POST /oidc/v1/token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<aws-web-identity-jwt>
subject_token_type=urn:ietf:params:oauth:token-type:jwt

A service principal federation policy binds an issuer URL + subject claim to a Databricks SP. Scopes on the returned token are controlled by the SP’s own permissions. Databricks’s Python SDK ships a built-in AwsStsTokenSource that calls boto3.client("sts").get_web_identity_token(...) and performs the exchange automatically.

2.4 GitHub Actions OIDC

GitHub Actions issues a signed OIDC JWT per job via ACTIONS_ID_TOKEN_REQUEST_TOKEN / ACTIONS_ID_TOKEN_REQUEST_URL. The issuer is https://token.actions.githubusercontent.com; the subject encodes the repository, ref, and environment:

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:my-org/my-repo:environment:prod",
  "aud": "https://mystack.palantirfoundry.com",
  "repository": "my-org/my-repo",
  "repository_owner": "my-org",
  "ref": "refs/heads/main",
  "environment": "prod",
  "job_workflow_ref": "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main",
  "jti": "example-id",
  "iat": 1632493567,
  "exp": 1632493867
}

Multipass should make it possible to register token.actions.githubusercontent.com as a trusted issuer in a TPA federation policy. The sub claim can be matched exactly (single repo + environment) or by prefix (all workflows in an org).

2.5 Foundry Outbound OIDC

Foundry already operates as an OIDC identity provider for outbound connections (Data Connection). When a sync runs, Foundry issues a JWT:

{
  "iss": "https://<stack>.palantirfoundry.com/foundry",
  "sub": "ri.magritte..source.7f3b8e21-4d9a-6c2e-1b7d-8a5f3c9e0b4f",
  "aud": "<target-system-audience>",
  "iat": 1700000000,
  "exp": 1700003600,
  "jti": "<unique-id>"
}

The source RID in sub lets the target system scope access without any stored credential. Multipass accepting the same token inbound is Flow 2 of this proposal.


3. Proposed Design

3.1 Flow 1 - External Workloads → Foundry (Inbound WIF)

Actors: Any compute workload running under an AWS IAM Role (ECS task, Lambda, EC2, GitHub Actions OIDC, etc.)

Token flow:

  AWS Workload                  AWS STS              Foundry Multipass         Foundry APIs
  (ECS/Lambda/EC2)                                                                         
       |                           |                         |                      |      
       |  get_web_identity_token() |                         |                      |      
       |  audience, tags=[scopes,  |                         |                      |      
       |  resources]               |                         |                      |      
       |-------------------------->|                         |                      |      
       |                           |                         |                      |      
       |   signed JWT              |                         |                      |      
       |   sub=IAM Role ARN        |                         |                      |      
       |   request_tags embedded   |                         |                      |      
       |<--------------------------|                         |                      |      
       |                           |                         |                      |      
       |       POST /multipass/api/oauth2/token              |                      |      
       |       grant_type=token-exchange                     |                      |      
       |       subject_token=<jwt>                           |                      |      
       |---------------------------------------------------->|                      |      
       |                           |                         |                      |      
       |                           |  GET /.well-known/      |                      |      
       |                           |  openid-configuration   |                      |      
       |                           |<------------------------|                      |      
       |                           |   public keys           |                      |      
       |                           |------------------------>|                      |      
       |                           |                         |                      |      
       |                           |              verify sig, exp, aud              |      
       |                           |              match federation policy           |      
       |                           |              intersect scopes + request_tags   |      
       |                           |                         |                      |      
       |        access_token (scoped, short-lived)           |                      |      
       |<----------------------------------------------------|                      |      
       |                           |                         |                      |      
       |                 Bearer token                        |                      |      
       |------------------------------------------------------------>|              |      
       |                           |                         |       |              |      
       |                           |                         |    response          |      
       |<-----------------------------------------------------------------|         |      
1. Workload calls AWS STS:
      sts.get_web_identity_token(audience="https://<stack>.palantirfoundry.com")
   → receives signed JWT  (sub = IAM Role ARN, iss = AWS account issuer)

2. Workload POSTs to Multipass:
      POST /multipass/api/oauth2/token
      Content-Type: application/x-www-form-urlencoded

      grant_type=urn:ietf:params:oauth:grant-type:token-exchange
      &subject_token=<jwt>
      &subject_token_type=urn:ietf:params:oauth:token-type:jwt
      [&scope=<requested-scopes>]     # optional; omit to get all policy-permitted scopes
      [&client_id=<tpa-client-id>]   # optional; required only if (iss, sub) is ambiguous across TPAs

3. Multipass validates:
      a. Fetch JWKS from iss + /.well-known/openid-configuration
      b. Verify JWT signature, exp, nbf, aud
      c. Look up TPA federation policies matching (iss, sub); if ambiguous across TPAs, require client_id
      d. Intersect requested scopes with policy's allowed scopes
      e. Read request_tags from JWT's "https://sts.amazonaws.com/" claim; apply tag-to-scope/resource mapping

4. Multipass returns:
      {
        "access_token": "<foundry-bearer-token>",
        "token_type": "Bearer",
        "expires_in": 3600,
        "scope": "<granted-scopes>"
      }

5. Workload calls Foundry APIs using the bearer token as normal.

Python example (external workload):

import boto3
import requests

FOUNDRY_HOST = "https://mystack.palantirfoundry.com"
TPA_CLIENT_ID = "my-pipeline-tpa"

def get_foundry_token(scopes: list[str], client_id: str | None = None) -> str:
    sts = boto3.client("sts")
    jwt = sts.get_web_identity_token(
        audience=FOUNDRY_HOST,
        signingAlgorithm="ES384",
    )["token"]

    body = {
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token": jwt,
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "scope": " ".join(scopes),
    }
    # client_id is only required when (iss, sub) is ambiguous across multiple TPAs.
    if client_id:
        body["client_id"] = client_id

    resp = requests.post(f"{FOUNDRY_HOST}/multipass/api/oauth2/token", data=body)
    resp.raise_for_status()
    return resp.json()["access_token"]

With downscoping via session tags:

AWS embeds tags as request_tags inside the https://sts.amazonaws.com/ claim of the resulting JWT.

CLI equivalent:

aws sts get-web-identity-token \
  --audience https://mystack.palantirfoundry.com \
  --signing-algorithm ES384 \
  --tags Key=scopes,Value="api:datasets-read api:ontologies-read" \
         Key=resources,Value="ri.compass.main.folder.0b6ae60f-468d-411d-8d71-ee9f18810672"

Python:

def get_foundry_token_downscoped(
    scopes: list[str],
    resources: list[str] | None = None,
    client_id: str | None = None,
) -> str:
    sts = boto3.client("sts")

    tags = [{"Key": "scopes", "Value": " ".join(scopes)}]
    if resources:
        tags.append({"Key": "resources", "Value": " ".join(resources)})

    jwt = sts.get_web_identity_token(
        audience=FOUNDRY_HOST,
        signingAlgorithm="ES384",
        tags=tags,
    )["token"]

    body = {
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token": jwt,
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "scope": " ".join(scopes),
    }
    if client_id:
        body["client_id"] = client_id

    resp = requests.post(f"{FOUNDRY_HOST}/multipass/api/oauth2/token", data=body)
    resp.raise_for_status()
    return resp.json()["access_token"]

Tags are optional. If neither scopes nor resources is present, the token carries all TPA scopes, identical to client_credentials with no scope parameter.


3.1.1 Example: GitHub Actions installing the OSDK package from the Foundry artifact repository

The GitHub JWT is presented directly to the token exchange endpoint and used as the bearer credential for the artifact repository pip install.

TPA federation policy for this workflow:

{
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:my-org/my-repo:environment:prod",
  "subject_match": "prefix",
  "audiences": ["https://mystack.palantirfoundry.com"]
}

subject_match: prefix allows matching all branches/environments of a repo (e.g. repo:my-org/my-repo:) or locking it down to a specific environment (repo:my-org/my-repo:environment:prod).

Workflow:

name: Install OSDK from Foundry artifact repository

on: [push]

permissions:
  id-token: write   # required to request the GitHub OIDC token
  contents: read

env:
  FOUNDRY_HOST: https://mystack.palantirfoundry.com
  TPA_CLIENT_ID: my-ci-tpa

jobs:
  install-sdk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Exchange GitHub identity for Foundry token
        id: foundry-auth
        run: |
          GITHUB_JWT=$(curl -sf -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$FOUNDRY_HOST" \
            | jq -r '.value')

          FOUNDRY_TOKEN=$(curl -sf "$FOUNDRY_HOST/multipass/api/oauth2/token" \
            -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
            -d "subject_token=$GITHUB_JWT" \
            -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
            -d "client_id=$TPA_CLIENT_ID" \
            | jq -r '.access_token')

          echo "::add-mask::$FOUNDRY_TOKEN"
          echo "token=$FOUNDRY_TOKEN" >> "$GITHUB_OUTPUT"

      - name: Install OSDK package
        env:
          FOUNDRY_TOKEN: ${{ steps.foundry-auth.outputs.token }}
        run: |
          pip install my_foundry_ontology_sdk==1.0.0 --upgrade \
            --index-url "https://user:$FOUNDRY_TOKEN@mystack.palantirfoundry.com/artifacts/api/repositories/ri.artifacts.main.repository.5b61b833-8067-48e5-b92a-0780a17b3671/contents/release/pypi/simple" \
            --extra-index-url "https://user:$FOUNDRY_TOKEN@mystack.palantirfoundry.com/artifacts/api/repositories/ri.foundry-sdk-asset-bundle.main.artifacts.repository/contents/release/pypi/simple"

No FOUNDRY_TOKEN secret is stored in GitHub. The token is derived fresh each run from the job’s ephemeral GitHub OIDC token and expires with the job.


3.2 Flow 2 - Code Inside Foundry → Foundry APIs (Self-Trust / Intra-Foundry WIF)

Problem: A Foundry Transform or Function that needs to call Foundry’s own platform APIs (e.g. Ontology write-back, dataset management) today must authenticate with a TPA client_secret materialised inside a Foundry Source or dataset. This is the same long-lived credential problem in a different location.

Foundry already issues OIDC tokens for outbound Data Connection syncs (sub = source RID). If Multipass optionally trusts its own OIDC issuer inbound, the same token can be exchanged for a scoped Foundry access token.

Token flow:

  Foundry Transform / Function        Foundry Multipass         Foundry APIs
          |                                   |                       |      
          |  source.get_session_credentials() |                       |      
          |  -> Foundry-issued OIDC token      |                       |      
          |     sub = source RID              |                       |      
          |     iss = Foundry stack           |                       |      
          |                                   |                       |      
          |  POST /multipass/api/oauth2/token |                       |      
          |  grant_type=token-exchange        |                       |      
          |  subject_token=<foundry-oidc-jwt> |                       |      
          |  client_id=<tpa-client-id>        |                       |      
          |---------------------------------->|                       |      
          |                                   |                       |      
          |                    verify iss == own OIDC issuer (opt-in) |      
          |                    match sub to federation policy         |      
          |                    enforce scope ceiling                  |      
          |                                   |                       |      
          |   access_token (scoped, short-lived)                      |      
          |<----------------------------------|                       |      
          |                                   |                       |      
          |             Bearer token          |                       |      
          |-------------------------------------------------->|       |      
          |                                   |               |       |      
          |                                   |            response   |      
          |<----------------------------------------------------------|      
1. Transform/Function retrieves the Foundry-issued OIDC token via the existing
   source.get_session_credentials() SDK call (same token Foundry uses for outbound syncs).
   Token: { iss: "https://<stack>.palantirfoundry.com/foundry",
             sub: "ri.magritte..source.<uuid>", aud: "<stack>", ... }

2. Code POSTs to Multipass (same endpoint as Flow 1):
      POST /multipass/api/oauth2/token
      grant_type=urn:ietf:params:oauth:grant-type:token-exchange
      &subject_token=<foundry-oidc-jwt>
      &subject_token_type=urn:ietf:params:oauth:token-type:jwt
      &client_id=<tpa-client-id>
      &scope=<requested-scopes>

3. Multipass validates:
      a. iss matches the stack's own OIDC issuer (self-trust is opt-in per TPA federation policy)
      b. sub matches the registered source RID in the policy
      c. Scopes are within the policy ceiling

4. Multipass returns a scoped Foundry access token (same response shape as Flow 1).

5. Code calls Foundry APIs.
import requests
from external_systems.sources import OauthCredentials, Refreshable, Source, SourceCredentials

FOUNDRY_HOST = "https://mystack.palantirfoundry.com"
TPA_CLIENT_ID = "my-pipeline-tpa"  # only the client_id, not a secret

def get_scoped_foundry_token(source: Source, scopes: list[str]) -> str:
    # Retrieve the Foundry-issued OIDC token for this source's execution context.
    refreshable: Refreshable[SourceCredentials] = source.get_session_credentials()
    credentials: SourceCredentials = refreshable.get()

    if not isinstance(credentials, OauthCredentials):
        raise ValueError("Expected OauthCredentials for OIDC-configured source")

    oidc_token: str = credentials.access_token

    resp = requests.post(
        f"{FOUNDRY_HOST}/multipass/api/oauth2/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "subject_token": oidc_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
            "client_id": TPA_CLIENT_ID,
            "scope": " ".join(scopes),
        },
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

4. API Changes

4.1 Token Exchange Endpoint (new grant_type)

Extend the existing POST /multipass/api/oauth2/token to accept:

Parameter Value
grant_type urn:ietf:params:oauth:grant-type:token-exchange (RFC 8693)
subject_token The external JWT (AWS web identity token or Foundry OIDC token)
subject_token_type urn:ietf:params:oauth:token-type:jwt
client_id Optional. The TPA client ID to target. Required only when the (iss, sub) pair matches federation policies on more than one TPA - Multipass returns invalid_request if disambiguation is needed but client_id is absent.
scope Space-separated OAuth2 scopes. Optional - if omitted, the token carries the full set of scopes permitted by the matched federation policy (identical behaviour to client_credentials with no scope parameter).

Neither client_secret nor any other client credential is required or accepted for this grant type (public client semantics per RFC 8693 §2.1).

Condition Error code
JWT signature invalid or expired invalid_grant
No matching federation policy unauthorized_client
Requested scope exceeds policy ceiling invalid_scope
Issuer JWKS fetch failed temporarily_unavailable

4.2 TPA Federation Policy (new TPA field)

Add a workload_identity_federation_policies list to the TPA registration model.

JSON schema (per policy entry):

{
  "issuer": "https://sts.amazonaws.com/accounts/123456789012",
  "subject": "arn:aws:iam::123456789012:role/my-pipeline-role",
  "subject_match": "exact",
  "audiences": ["https://mystack.palantirfoundry.com"],
  "allowed_scopes": ["api:datasets-read", "api:ontologies-read"],
  "tag_mappings": {
    "scopes": { "type": "oauth2_scope_list" },
    "resources": { "type": "compass_resource_rid_list" }
  }
}
Field Required Description
issuer Yes Exact OIDC issuer URL. Multipass fetches JWKS from {issuer}/.well-known/openid-configuration.
subject Yes The sub claim to match.
subject_match No exact (default) or prefix. Prefix match useful for org-level IAM role patterns.
audiences Yes At least one aud value the presented JWT must contain.
allowed_scopes No Ceiling on scopes this policy may grant. If omitted, the TPA’s own registered scopes are the ceiling.
tag_mappings No Declares which session tag keys to process and how (oauth2_scope_list or compass_resource_rid_list).

Self-trust policy (Flow 2): To enable intra-Foundry WIF, an admin registers a policy with issuer set to the stack’s own Foundry OIDC issuer URL. Multipass does not trust its own issuer by default; it must be explicitly configured per TPA.

4.3 Developer Console / Control Panel Changes

  • New “Workload Identity Federation” tab on TPA configuration page.
  • List view of registered federation policies with issuer, subject, and audience displayed.
  • Ability to add / edit / delete policies.
  • Audit log entries for token exchanges performed via WIF (distinct action type from client-credentials exchanges).

+1 to the pain point outlined in this FR