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:
- Read the external token from the specified file
- Perform RFC 8693 token exchange to obtain a Package Manager token
- Use the exchanged token for authentication
- 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
- An external system (like GitHub Actions) obtains an OpenID Connect token from its own identity provider
- The system exchanges the external token for a Package Manager token using RFC 8693 token exchange
- The Package Manager token is used to authenticate API requests
- 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 beurn:ietf:params:oauth:grant-type:token-exchangesubject_token: The external OpenID Connect token to exchangesubject_token_type: Must beurn:ietf:params:oauth:token-type:id_tokenscope: 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 = trueThe Subject field should match the expected subject claim pattern from GitHub Actions tokens. Common patterns include:
repo:org/repo:ref:refs/heads/mainfor main branchrepo:org/repo:pull_requestfor pull requestsrepo: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.gzStep 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.gzAzure 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.
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
- In the Azure Portal, go to Microsoft Entra ID > App registrations > New registration (see Register an application)
- Name it (e.g.,
PPM OIDC Federation) - Leave defaults and click Register
- 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.
- In Azure DevOps, go to Project Settings > Service connections
- Click Create service connection, then select Azure Resource Manager and click Next
- Set Identity type to App registration or managed identity (manual)
- Set Credential to Workload Identity Federation
- In Step 1: Basics, fill in:
- Service Connection Name: e.g.,
PackageManagerFederation - Directory (tenant) ID: your Entra tenant ID from Step 2
- Service Connection Name: e.g.,
- 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
- Check Grant access permission to all pipelines (or grant per-pipeline later)
- Click Save without verification. Package Manager does not require Azure resource access, so verification will fail
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.
- Back in the Azure Portal, go to your app registration from Step 2
- Go to Certificates & secrets > Federated credentials > Add credential
- Select Other issuer as the scenario
- 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
- Issuer:
- Click Add
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 = trueUsernameClaim = "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.
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):
- Go to Pipelines > New pipeline
- Select the Git repository that contains the code you want to build using Package Manager
- Choose Starter pipeline (or Existing Azure Pipelines YAML file if you already have one)
- 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)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.
The Service Connection GUID can be found in the URL when viewing the service connection in Azure DevOps: _settings/adminservices?resourceId={guid}.
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:*"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:
- Validates the request format and required parameters
- Parses the subject token as a JWT
- Checks if the token’s issuer matches any configured
IdentityFederationprovider - Validates the token signature against the issuer’s public keys
- Checks token expiration and other standard JWT claims
- Validates provider-specific claims (audience, subject, authorized party) if configured
- Applies scope mappings based on groups, roles, or base scopes
- 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 = trueCommon Issues
- Token exchange fails: Verify that the issuer URL exactly matches the token’s
issclaim - Audience mismatch: Ensure the
audiencesetting matches what your external system specifies when requesting tokens - Subject pattern: Check that your subject regex pattern correctly matches the expected token subjects
- Token expiration: Verify that tokens haven’t expired before reaching Package Manager
- Claims missing: Ensure your external provider includes the expected group or role claims in tokens
- 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-authSecurity Considerations
- Token exchange validation: Package Manager thoroughly validates external tokens before issuing internal tokens
- Audience validation: Always configure the
Audiencesetting to prevent token reuse across different systems - Subject restrictions: Use
Subjectpatterns 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
For detailed examples of scope mapping configurations, see the OpenID Connect Scope Mapping Guide.