OpenID Connect with Identity Federation

Advanced

Overview

Package Manager’s identity federation feature enables seamless integration with external systems that use OpenID Connect tokens for authentication. This powerful capability allows Package Manager to accept and validate tokens issued by external identity providers, eliminating the need for separate credential management across different systems.

Common integration scenarios include:

  • GitHub Actions workflows accessing Package Manager repositories during CI/CD pipelines
  • External CI/CD systems (GitLab CI, Jenkins, Azure DevOps) authenticating with their own identity providers
  • Service accounts from partner organizations or external systems
  • Cross-platform integrations where applications authenticate through multiple identity providers
  • Automated deployment pipelines requiring secure package access

Identity federation differs from standard OpenID Connect authentication in that it focuses on API access rather than user login sessions. While OpenID Connect authentication allows users to sign in through external providers, identity federation allows Package Manager to trust and validate tokens that external systems have already obtained from their own identity providers.

This approach provides enhanced security and operational efficiency by:

  • Eliminating the need to manage separate credentials for each system
  • Leveraging existing identity infrastructure investments
  • Providing fine-grained access control through scope mapping
  • Supporting RFC 8693 token exchange for secure token conversion
  • Enabling audit trails across integrated systems

Package Manager CLI Integration

The Package Manager CLI (rspm) provides built-in support for identity federation through the PACKAGEMANAGER_IDENTITY_TOKEN_FILE environment variable. When this environment variable is set and points to a file containing an external OpenID Connect token, the CLI will automatically:

  1. Read the external token from the specified file
  2. Perform RFC 8693 token exchange to obtain a Package Manager token
  3. Use the exchanged token for authentication
  4. Execute the requested command

This seamless integration makes it easy to use Package Manager from CI/CD environments that already provide OpenID Connect tokens. For detailed examples of using the CLI with identity federation, see:

How Identity Federation Works

  1. An external system (like GitHub Actions) obtains an OpenID Connect token from its own identity provider
  2. The system exchanges the external token for a Package Manager token using RFC 8693 token exchange
  3. The Package Manager token is used to authenticate API requests
  4. Package Manager validates requests and grants access based on the configured scope mappings

Token Exchange (RFC 8693)

Package Manager implements RFC 8693 OAuth 2.0 Token Exchange to convert external OpenID Connect tokens into Package Manager access tokens. This is necessary because you cannot use external ID tokens directly as authorization headers - they must first be exchanged for Package Manager tokens.

Token Exchange Endpoint

The token exchange endpoint is available at https://HOST:PORT/__api__/token and accepts the following parameters:

  • grant_type: Must be urn:ietf:params:oauth:grant-type:token-exchange
  • subject_token: The external OpenID Connect token to exchange
  • subject_token_type: Must be urn:ietf:params:oauth:token-type:id_token
  • scope: Required but can be any value (scopes are determined by identity federation configuration)

Example Token Exchange

# Exchange an external OIDC token for a Package Manager token
curl -X POST 'https://packagemanager.example.com/__api__/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$EXTERNAL_TOKEN" \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:id_token'

# Response will contain the Package Manager access token
{
  "access_token": "<ppm_token_here>",
  "issued_token_type":"urn:ietf:params:oauth:token-type:jwt",
  "token_type": "Bearer",
  "expires_in": 3600
}

Configuration Overview

Identity federation is configured using IdentityFederation sections in the Package Manager configuration. Each section defines a separate identity provider that Package Manager should trust.

Basic Configuration Example

/etc/rstudio-pm/rstudio-pm.gcfg
; Example: Trust GitHub Actions tokens
[IdentityFederation "github-actions"]
Issuer = "https://token.actions.githubusercontent.com"
Audience = "https://github.com/your-org"
Subject = "repo:your-org/your-repo:.*"
Scope = "repos:read:*"

; Example: Trust tokens from another identity provider
[IdentityFederation "external-ci"]
Issuer = "https://auth.company.com"
Audience = "https://build.example.com"
Subject = "^service-account-.*"
Scope = "sources:write:*"

GitHub Actions Integration

A common use case for identity federation is allowing GitHub Actions workflows to access Package Manager repositories. Here’s how to configure this:

Step 1: Configure Identity Federation for GitHub

/etc/rstudio-pm/rstudio-pm.gcfg
[IdentityFederation "github-actions"]
Issuer = "https://token.actions.githubusercontent.com"
Audience = "https://github.com/your-org"
Subject = "repo:your-org/your-repo:pull_request"
Scope = "repos:read:*"
Scope = "sources:write:*"

; Optional: Enable logging for troubleshooting
Logging = true
Note

The Subject field should match the expected subject claim pattern from GitHub Actions tokens. Common patterns include:

  • repo:org/repo:ref:refs/heads/main for main branch
  • repo:org/repo:pull_request for pull requests
  • repo:org/repo:.* for any ref in the repository

Step 2: Configure GitHub Actions Workflow

Here’s a complete example of a GitHub Actions workflow that uses identity federation to access Package Manager:

name: Package Manager Integration
on:
  pull_request:
    branches: ['**']
  workflow_dispatch:

permissions:
  id-token: write   # Required for requesting OIDC tokens
  contents: read    # Required for actions/checkout

jobs:
  test-package-manager:
    name: Test Package Manager Access
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Fetch OIDC Token
        id: fetch-token
        run: |
          set -eo pipefail
          
          # Request OIDC token from GitHub
          RAW_TOKEN_JSON=$(curl -sf -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://github.com/your-org")
          
          # Extract token value and save securely
          echo "${RAW_TOKEN_JSON}" | jq -r '.value' > ${{ runner.temp }}/github_token
          echo "GitHub OIDC token obtained successfully"

      - name: Exchange Token with Package Manager
        id: exchange-token
        run: |
          # Read the GitHub OIDC token
          GITHUB_TOKEN=$(cat ${{ runner.temp }}/github_token)
          
          # Exchange for Package Manager token
          RESPONSE=$(curl -sf -X POST \
            'https://packagemanager.example.com/__api__/token' \
            --header 'Content-Type: application/x-www-form-urlencoded' \
            --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
            --data-urlencode "subject_token=$GITHUB_TOKEN" \
            --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:id_token')
          
          # Extract Package Manager token
          PPM_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
          echo "::add-mask::$PPM_TOKEN"
          echo "ppm_token=$PPM_TOKEN" >> $GITHUB_OUTPUT

      - name: Install Python packages from Package Manager
        run: |
          # Create virtual environment
          python -m venv venv
          source venv/bin/activate
          
          # Configure pip to use Package Manager with token authentication
          export PIP_INDEX_URL="https://__token__:${{ steps.exchange-token.outputs.ppm_token }}@packagemanager.example.com/pypi/latest/simple/"
          
          # Install packages
          pip install requests numpy pandas

      - name: Upload package to Package Manager
        if: github.event_name == 'push'
        run: |
          # Install twine for uploads
          pip install twine
          
          # Configure twine with Package Manager token
          export TWINE_REPOSITORY_URL="https://packagemanager.example.com/upload/pypi/local-source"
          export TWINE_USERNAME="__token__"
          export TWINE_PASSWORD="${{ steps.exchange-token.outputs.ppm_token }}"
          
          # Upload package (assuming package is built)
          twine upload dist/*.tar.gz

Step 3: Alternative - Using Environment Variables

You can also configure Package Manager authentication using environment variables:

      - name: Set Package Manager Environment
        run: |
          # For tools that support Package Manager environment variables
          echo "PACKAGEMANAGER_IDENTITY_TOKEN_FILE=${{ runner.temp }}/github_token" >> $GITHUB_ENV
          echo "PACKAGEMANAGER_ADDRESS=https://packagemanager.example.com" >> $GITHUB_ENV

      - name: Use token exchange with environment variables
        run: |
          # Create and activate a Python virtual environment.
          python -m venv venv
          source venv/bin/activate

          # Install necessary python packages
          pip install --upgrade pip
          pip install twine

          # Install the package
          echo "Installing posit-keyring..."
          pip install posit-keyring

          # Check keyring is installed and pointing to the correct backend
          echo "Checking keyring backend..."
          which keyring
          keyring --list-backends

          # Set environment variables for the tests
          echo "Setting necessary environment variables..."
          export PIP_INDEX_URL="https://__token__@packagemanager.example.com/pypi-auth/latest/simple/"
          export TWINE_REPOSITORY_URL="https://packagemanager.example.com/upload/pypi/local-python"
          export TWINE_USERNAME="__token__"

          # Run pip test against Package Manager
          echo "Testing pip install..."
          pip install chatlas

          # Run twine test against Package Manager
          echo "Testing twine upload..."
          twine upload tests/data/posit_test_package_example-1.0.0.tar.gz

Azure DevOps Pipelines Integration

Azure DevOps Pipelines can authenticate with Package Manager using identity federation. Azure DevOps uses Workload Identity Federation through Microsoft Entra ID (formerly Azure AD). The tokens your pipeline obtains are Entra ID tokens issued by login.microsoftonline.com. For other Azure integrations (storage, databases), see the Microsoft Azure integration guide.

Important

Do not use Entra ID Client Credentials (machine-to-machine) flows to obtain tokens for Package Manager. Client Credentials grants only issue Access Tokens, not ID tokens, and cannot be used for identity federation.

Instead, use Azure DevOps Pipelines with a Workload Identity Federation service connection, which obtains proper tokens through the pipeline’s built-in OIDC mechanism.

Step 1: Find Your Azure DevOps Organization GUID

The federated credential (Step 4) requires your Azure DevOps organization’s Globally Unique Identifier (GUID). Find it with:

curl -s "https://dev.azure.com/{organization-name}/_apis/connectiondata" | jq -r '.instanceId'

Note the resulting GUID. You will use it to form the issuer URL: https://vstoken.dev.azure.com/{organization-guid}

Step 2: Create an App Registration in Microsoft Entra ID

  1. In the Azure Portal, go to Microsoft Entra ID > App registrations > New registration (see Register an application)
  2. Name it (e.g., PPM OIDC Federation)
  3. Leave defaults and click Register
  4. Note the Application (client) ID and your Tenant ID (found on the Entra ID Overview page)

You will add a federated credential to this app registration after creating the service connection in Step 3.

Step 3: Create a Service Connection in Azure DevOps

See Manage service connections in the Azure DevOps documentation.

  1. In Azure DevOps, go to Project Settings > Service connections
  2. Click Create service connection, then select Azure Resource Manager and click Next
  3. Set Identity type to App registration or managed identity (manual)
  4. Set Credential to Workload Identity Federation
  5. In Step 1: Basics, fill in:
    • Service Connection Name: e.g., PackageManagerFederation
    • Directory (tenant) ID: your Entra tenant ID from Step 2
  6. Click Next. In Step 2: App registration details, fill in:
    • Subscription ID and Subscription Name: any valid Azure subscription (required by the form, but not used for Package Manager access)
    • Application (client) ID: the Application (client) ID from Step 2
  7. Check Grant access permission to all pipelines (or grant per-pipeline later)
  8. Click Save without verification. Package Manager does not require Azure resource access, so verification will fail
Note

Note the Service Connection Name exactly as entered. It must match the Subject identifier in the federated credential (Step 4).

Step 4: Add a Federated Credential to the App Registration

See Configure an app to trust an external identity provider in the Microsoft documentation.

  1. Back in the Azure Portal, go to your app registration from Step 2
  2. Go to Certificates & secrets > Federated credentials > Add credential
  3. Select Other issuer as the scenario
  4. Fill in:
    • Issuer: https://vstoken.dev.azure.com/{organization-guid} (the GUID from Step 1)
    • Subject identifier: sc://{organization-name}/{project-name}/{service-connection-name} (e.g., sc://MyOrg/MyProject/PackageManagerFederation; must match the service connection name from Step 3)
    • Audience: leave the default api://AzureADTokenExchange
  5. Click Add
Note

The Azure Portal may display different recommended values for Issuer and Subject Identifier based on your configuration. Use the values above. The Issuer must be the Azure DevOps STS URL with your organization GUID, and the Subject Identifier must follow the sc:// format.

Step 5: Configure Identity Federation in Package Manager

The tokens issued through this flow are Entra ID tokens, so Package Manager must be configured with the Entra ID issuer:

/etc/rstudio-pm/rstudio-pm.gcfg
[IdentityFederation "azure-devops"]
Issuer = "https://login.microsoftonline.com/{tenant-id}/v2.0"
Audience = "fb60f99c-7a34-4190-8149-302f77469936"
AuthorizedParty = "499b84ac-1321-427f-aa17-267ca6975798"
Subject = ".*"
Scope = "repos:read:*"
UsernameClaim = "oid"

; Optional: Enable logging for troubleshooting
Logging = true
Note

UsernameClaim = "oid" is required because these are app tokens (machine-to-machine), not user tokens. The default preferred_username claim is not present in Entra ID app tokens. The oid claim (Object ID) provides a stable identifier for the service principal.

Note

The Audience (fb60f99c-7a34-4190-8149-302f77469936) is the GUID form of api://AzureADTokenExchange, the Microsoft Entra ID Token Exchange Endpoint. The AuthorizedParty (499b84ac-1321-427f-aa17-267ca6975798) is the Azure DevOps first-party application ID. These values are constant across all tenants and organizations within Azure Commercial Cloud. Sovereign clouds (Azure Government, Azure China 21Vianet, Azure Germany) use different application IDs. Consult Microsoft’s national cloud documentation for the correct values in your environment.

The Subject claim in these tokens uses an opaque Entra ID format rather than a human-readable identifier, so a wildcard pattern (.*) is typical.

Step 6: Configure the Azure DevOps Pipeline

Azure DevOps pipelines are defined as YAML files stored in a Git repository. To create a pipeline (see Create your first pipeline):

  1. Go to Pipelines > New pipeline
  2. Select the Git repository that contains the code you want to build using Package Manager
  3. Choose Starter pipeline (or Existing Azure Pipelines YAML file if you already have one)
  4. Replace or update the pipeline YAML with the configuration below

The pipeline fetches the OIDC token using the Distributed Task REST API. Azure DevOps requires an explicit service connection reference before it will authorize OIDC token generation. The AzureCLI@2 task provides this reference:

trigger: none

pool:
  vmImage: 'ubuntu-latest'

variables:
  serviceConnectionId: '{service-connection-guid}'  # From the service connection URL in ADO settings
  ppmUrl: 'packagemanager.example.com'      # Your Package Manager server URL

steps:
  # Reference the service connection so ADO authorizes OIDC token generation
  - task: AzureCLI@2
    displayName: 'Authorize service connection'
    inputs:
      azureSubscription: 'PackageManagerFederation'
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: 'echo "Service connection authorized"'
    continueOnError: true

  - script: |
      set -eo pipefail

      # Fetch the OIDC token from the Distributed Task REST API
      OIDC_URL="$COLLECTION_URI$PROJECT_ID/_apis/distributedtask/hubs/build/plans/$PLAN_ID/jobs/$JOB_ID/oidctoken?serviceConnectionId=$SC_ID&api-version=7.1-preview.1"
      ID_TOKEN=$(curl -sf -X POST -H "Authorization: Bearer $SYSTEM_ACCESSTOKEN" -H "Content-Type: application/json" -d '{}' "$OIDC_URL" | jq -r .oidcToken)

      # Exchange the OIDC token for a Package Manager token
      RESPONSE=$(curl -sf -X POST "https://$PPM_URL/__api__/token" --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' --data-urlencode "subject_token=$ID_TOKEN" --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:id_token')

      PPM_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
      echo "##vso[task.setvariable variable=PPM_TOKEN;issecret=true]$PPM_TOKEN"
    displayName: 'Authenticate with Package Manager'
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)
      COLLECTION_URI: $(System.TeamFoundationCollectionUri)
      PROJECT_ID: $(System.TeamProjectId)
      PLAN_ID: $(System.PlanId)
      JOB_ID: $(System.JobId)
      SC_ID: $(serviceConnectionId)
      PPM_URL: $(ppmUrl)

  - script: |
      export PIP_INDEX_URL="https://__token__:$PPM_TOKEN@$PPM_URL/pypi/latest/simple/"
      pip install requests numpy pandas
    displayName: 'Install Python packages from Package Manager'
    env:
      PPM_TOKEN: $(PPM_TOKEN)
      PPM_URL: $(ppmUrl)
Important

The AzureCLI@2 task will show an error because az login fails. This is expected and can be ignored. The task exists only to declare the service connection reference so ADO authorizes OIDC token generation for the pipeline. The actual token is fetched by the script step using the REST API, which works independently of az login. The continueOnError: true setting ensures the pipeline proceeds past the expected error.

Note

The Service Connection GUID can be found in the URL when viewing the service connection in Azure DevOps: _settings/adminservices?resourceId={guid}.

Note

The first time a new pipeline runs, Azure DevOps will pause and prompt you to permit the pipeline to use the service connection, even if you checked “Grant access permission to all pipelines” in Step 3. Click Permit to allow it. This is a one-time approval per pipeline.

Azure DevOps Token Claims Reference

Tokens obtained through Azure DevOps Workload Identity Federation are Entra ID v2.0 tokens with the following relevant claims:

Claim Description Value
iss Issuer (Microsoft Entra ID) https://login.microsoftonline.com/{tenant-id}/v2.0
aud Audience, Entra ID Token Exchange Endpoint (api://AzureADTokenExchange) fb60f99c-7a34-4190-8149-302f77469936
azp Authorized party (Azure DevOps first-party application) 499b84ac-1321-427f-aa17-267ca6975798
sub Subject (opaque Entra ID identifier) Opaque string (not human-readable)
roles App roles assigned to the service principal Configured in Entra ID app registration

Advanced Configuration Options

Filtering by Subject

You can restrict which tokens are accepted based on the subject claim:

/etc/rstudio-pm/rstudio-pm.gcfg
; Only accept tokens for specific service accounts
[IdentityFederation "service-accounts"]
Issuer = "https://auth.company.com"
Audience = "some-audience"
Subject = "^service-account-(ci|deployment)$"
Scope = "sources:write:*"

Filtering by Authorized Party

Use the AuthorizedParty setting to validate the azp claim:

/etc/rstudio-pm/rstudio-pm.gcfg
[IdentityFederation "external-system"]
Issuer = "https://auth.external.com"
Audience = "some-audience"
Subject = "^service-account-(ci|deployment)$"
AuthorizedParty = "packagemanager-client"
Scope = "repos:read:*"

Group and Role-Based Scope Mapping

Similar to OpenID Connect authentication, you can map groups and roles from identity federation tokens to Package Manager scopes:

/etc/rstudio-pm/rstudio-pm.gcfg
[IdentityFederation "github-actions"]
Issuer = "https://token.actions.githubusercontent.com"
Audience = "https://github.com/your-org"
Subject = ".*"
GroupsClaim = "repository_owner"

; Map repository owners to different scopes
[GroupToScopeMapping "data-science-team"]
Provider = "github-actions"
Scope = "repos:read:*"
Scope = "sources:write:*"

[GroupToScopeMapping "public-repos"]
Provider = "github-actions"
Scope = "repos:read:*"

Multiple Identity Providers

You can configure multiple identity federation providers:

/etc/rstudio-pm/rstudio-pm.gcfg
; GitHub Actions for CI/CD
[IdentityFederation "github-actions"]
Issuer = "https://token.actions.githubusercontent.com"
Audience = "https://github.com/your-org"
Subject = "repo:your-org/your-repo:.*"

; The "data-science-team" group mapped to the "github-actions" federated identity
[GroupToScopeMapping "data-science-team"]
Provider = "github-actions"
Scope = "repos:read:*"

; GitLab CI for another team
[IdentityFederation "gitlab-ci"]
Issuer = "https://gitlab.company.com"
Audience = "packagemanager"
Subject = "repo:your-org/your-repo:.*"
Scope = "repos:read:repo-1"

; The "data-science-team" group mapped to the "gitlab-ci" federated identity
[GroupToScopeMapping "data-science-team"]
Provider = "gitlab-ci"
Scope = "repos:read:*"
Scope = "sources:write:*"

; External partner system
[IdentityFederation "partner-system"]
Issuer = "https://auth.partner.com"
Audience = "partner-audience"
Subject = "^partner-service-.*"
Scope = "repos:read:*"

; Internal service accounts
[IdentityFederation "internal-services"]
Issuer = "https://auth.company.com"
Audience = "internal-audience"
Subject = "^internal-service-.*"
AuthorizedParty = "packagemanager-integration"
Scope = "sources:write:*"
Scope = "metadata:admin"

Token Validation Process

When Package Manager receives a token exchange request, it:

  1. Validates the request format and required parameters
  2. Parses the subject token as a JWT
  3. Checks if the token’s issuer matches any configured IdentityFederation provider
  4. Validates the token signature against the issuer’s public keys
  5. Checks token expiration and other standard JWT claims
  6. Validates provider-specific claims (audience, subject, authorized party) if configured
  7. Applies scope mappings based on groups, roles, or base scopes
  8. Issues a Package Manager access token with the appropriate scopes

When Package Manager receives an API request with a Bearer token, it validates the Package Manager token and grants access based on the token’s scopes.

Troubleshooting Identity Federation

Enable Verbose Logging

/etc/rstudio-pm/rstudio-pm.gcfg
[IdentityFederation "your-provider"]
Logging = true

Common Issues

  1. Token exchange fails: Verify that the issuer URL exactly matches the token’s iss claim
  2. Audience mismatch: Ensure the audience setting matches what your external system specifies when requesting tokens
  3. Subject pattern: Check that your subject regex pattern correctly matches the expected token subjects
  4. Token expiration: Verify that tokens haven’t expired before reaching Package Manager
  5. Claims missing: Ensure your external provider includes the expected group or role claims in tokens
  6. Invalid grant type: Ensure you’re using the exact grant type urn:ietf:params:oauth:grant-type:token-exchange

Testing Token Exchange

You can test the token exchange process manually:

# 1. Get a token from your external provider (this varies by provider)
EXTERNAL_TOKEN="your-oidc-token-here"

# 2. Exchange it for a Package Manager token
PPM_TOKEN_RESPONSE=$(curl -sf -X POST \
  'https://packagemanager.example.com/__api__/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$EXTERNAL_TOKEN" \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:id_token')

# 3. Extract the Package Manager token
PPM_TOKEN=$(echo "$PPM_TOKEN_RESPONSE" | jq -r '.access_token')

# 4. Test API access with the Package Manager token
curl -H "Authorization: Bearer $PPM_TOKEN" \
  https://packagemanager.example.com/__api__/verify-auth

Security Considerations

  • Token exchange validation: Package Manager thoroughly validates external tokens before issuing internal tokens
  • Audience validation: Always configure the Audience setting to prevent token reuse across different systems
  • Subject restrictions: Use Subject patterns to limit which service accounts or users can access Package Manager
  • Scope minimization: Grant only the minimum necessary scopes for each identity provider
  • Token expiration: External tokens should have short lifetimes; Package Manager tokens inherit reasonable expiration times
  • Secure token storage: Never log or expose tokens in plain text
  • Logging: Monitor authentication logs for unusual activity
Tip

For detailed examples of scope mapping configurations, see the OpenID Connect Scope Mapping Guide.

Back to top