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

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