PIA (Project Identity Authority) is an authentication broker for Eclipse Foundation projects. It allows projects to publish artifacts, and to proof that the artifact came from the project's build infrastructure, using OpenID Connect (OIDC).
In the initial use case, described in this document, PIA serves as intermediary between Eclipse build infrastructure (GitHub Actions, Jenkins Pipelines) and the Eclipse DependencyTrack registry. This enables project-level access control for SBOM uploads to DependencyTrack. Later, PIA may extend to other use cases that benefit from this authentication.
DependencyTrack lacks project-level access control:
- Any team with upload permissions can publish SBOMs for all projects
- Shared secrets management is complex and error-prone
- Workarounds exist but are restricted to GitHub Actions
PIA implements a "Trusted Publishing" architecture using OIDC to:
- Authenticate publishers using short-lived identity tokens
- Verify project ownership through Identity Provider claims
- Eliminate need for long-lived credentials and manual secrets management between build infrastructure and DependencyTrack
- Support multiple build platforms (GitHub Actions, Jenkins, extensible to others)
- Provide a scalable architecture (extensible to other artifacts and registries)
sequenceDiagram
participant Publisher as Publisher<br/>(GHA, Jenkins Pipeline)
participant IdP as Identity Provider<br/>(GitHub, Jenkins Controller)
participant PIA as PIA<br/>
participant dp as DependencyTrack<br/>
Publisher->>IdP: 1. Request ID token
IdP->>Publisher: 2. Return ID token
Publisher->>PIA: 3. Post SBOM + token
PIA->>IdP: 4. Request OIDC config
IdP->>PIA: 5. Return OIDC config
PIA->>IdP: 6. Request JWKs
IdP->>PIA: 7. Return JWKs
PIA->>PIA: 8. Authenticate
Note over PIA: - Verify token<br/>- Match claims with projects database
PIA->>dp: 9. Upload SBOM
dp-->>PIA: Success
PIA-->>Publisher: Success
- GitHub Action workflow or Jenkins Pipeline
- Generates SBOMs
- Requests identity token from its Identity Provider
- Sends SBOM + token to PIA
- GitHub: Central OIDC provider (provided by GitHub)
- Jenkins: Per-instance OIDC Provider (enabled via plugin)
- Issues cryptographically signed JWT tokens with claims about publisher identity
- Serves OIDC configuration and public keys from well known location
- Provides REST API for SBOM upload
- Verifies identity tokens using OIDC discovery protocol
- Authenticates SBOM based on OIDC claims and internal projects database
- Publishes SBOM to DependencyTrack on behalf of authenticated projects
- Receives SBOM from PIA after successful authentication
- Identity Provider trusts Publisher because:
- Each GitHub Publisher has a built-in auth token for the central GitHub Identity Provider (managed by GitHub)
- Each Jenkins Publisher has its own Identity Provider running on the same Jenkins instance (managed by Eclipse Foundation)
- PIA trusts Identity Provider because PIA has a projects database for Identity Provider (issuer) URLs (managed by Eclipse Foundation)
- DependencyTrack trusts PIA because PIA has an auth token for the DependencyTrack API (managed by Eclipse Foundation)
-
Token Request: Publisher requests ID token from Identity Provider
- GitHub: Uses
ACTIONS_ID_TOKEN_REQUEST_URLandACTIONS_ID_TOKEN_REQUEST_TOKEN - Jenkins: Uses credentials binding plugin with pre-configured credential
- GitHub: Uses
-
Token Issuance: Identity Provider returns JWT with minimal required claims:
iss: Issuer URL (identifies Identity Provider)exp: Expiration timestampiat: Issued At timestampaud: Audience (must match PIA's expected audience)- Platform-specific:
- GitHub:
repository,repository_owner,repository_owner_ididentify the publishing repository - Jenkins:
issidentifies the project via URL path
- GitHub:
-
Publish Request: Publisher sends POST request to PIA:
Authorizationheader: Bearer token containing OIDC token (RFC6750)product_name: Name of product for which the SBOM is produced. This field is required by DependencyTrack to aggregate SBOMs by product within a project. We ask the Publisher to provide it, so that we don't have to parse the SBOM (seemetadata.component.name).product_version: Version of product for which the SBOM was produced. This field is required by DependencyTrack. We ask the Publisher to provide it, so that we don't have to parse the SBOM (seemetadata.component.version).bom: Base64-encoded CycloneDX JSON SBOMis_latest: Whether this SBOM should be marked as the latest version in DependencyTrack (optional, default:true)
-
Token Verification and Authentication: PIA verifies POST data token
- see 3.1.1. Token Verification and Authentication Flow
-
DependencyTrack Project Resolution: PIA looks up the DependencyTrack project by matching
namewithproduct_namefrom the POST data, where the DependencyTrack project shares the sameef_project_idas the authenticated workload. This provides thedt_parent_uuidfor the DependencyTrack upload. -
SBOM Publishing: PIA sends upload request to DependencyTrack:
Content-Type:application/jsonX-Api-Key: Use internally stored DependencyTrack access tokenprojectName: Useproduct_namefrom POST dataprojectVersion: Useproduct_versionfrom POST data.parentUUID: Useparent_uuidfrom matched DependencyTrack projectautoCreate:true(creates new subcategory for newprojectName)isLatest: Useis_latestfrom POST databom: Usebomfrom POST data
- Basic POST Data Validation
- Validate basic POST data schema
- Issuer Validation
- Decode token without any verification
- Extract issuer
- Verify issuer is known
- GitHub: Full match:
https://token.actions.githubusercontent.com - Jenkins: Prefix match:
https://ci.eclipse.org
- GitHub: Full match:
- Token Key Discovery
- Fetch OIDC configuration from
{issuer}/.well-known/openid-configuration - Extract
jwks_urifrom configuration - Fetch public keys from
jwks_uri - Match key using
kidclaim from token header
- Fetch OIDC configuration from
- Token Validation and Signature Verification
- Verify generally required claims are present
- Verify token issued at time and expiry
- Verify audience matches expected value
- Verify signature using RSA256
- Workload Matching
- Find workload type by issuer
- Find workload by matching verified token claims against workload database:
- GitHub: match
repo_owner,repo_name,repo_owner_id - Jenkins: match
issuer
- GitHub: match
Accepts sbom uploads with OIDC authentication.
Request Headers:
Authorization: Bearer <JWT ID token>
Content-Type: application/json
Request Body:
{
"product_name": "string", // Eclipse product name
"product_version": "string", // Eclipse product version
"bom": "string", // CycloneDX JSON SBOM (base64-encoded)
"is_latest": true // Mark as latest version in DependencyTrack (optional, default: true)
}Response:
200 OK: SBOM accepted by DependencyTrack. Body provides the URL the publisher should poll for processing status:{ "polling_url": "https://sbom.eclipse.org/api/v1/bom/token/<token>" }401 Unauthorized:- Invalid Authorization header format
- Invalid token
- Expired token
- Token verification error
- Issuer not allowed
- No matching workload found for token claims
- No matching DependencyTrack project found
422 Unprocessable Entity: Missing Authorization header or invalid JSON502: DependencyTrack upload request failed*: Relay DependencyTrack status code and body verbatim (non-2xx)
PIA requires settings for:
-
Application: Loaded from environment variables with
PIA_prefix:PIA_DEPENDENCY_TRACK_API_KEY: DependencyTrack API key (required)PIA_DATABASE_URL: PostgreSQL connection string (required)PIA_DEPENDENCY_TRACK_URL: DependencyTrack SBOM upload URL (default:https://sbom.eclipse.org/api/v1/bom)PIA_EXPECTED_AUDIENCE: Expected audience for OIDC tokens (default:pia.eclipse.org)
-
Database: PostgreSQL database storing Eclipse Foundation projects, workloads, and DependencyTrack projects. See section 4.3 for the data model.
An Eclipse Foundation project. Groups workloads and DependencyTrack projects.
Uses the Eclipse project identifier directly as primary key. Foreign keys
referencing this table use ON UPDATE CASCADE to propagate project ID changes.
| Column | Type | Notes |
|---|---|---|
id |
String PK | Eclipse project identifier (e.g. technology.foo) |
A CI/CD entity that can upload SBOMs. Uses joined-table inheritance with a
type discriminator column.
| Column | Type | Notes |
|---|---|---|
id |
Serial PK | |
ef_project_id |
FK → eclipse_foundation_projects.id | |
type |
String | Discriminator: github or jenkins |
Issuer is always https://token.actions.githubusercontent.com (constant, not
stored).
| Column | Type | Notes |
|---|---|---|
id |
FK → workloads.id | |
repo_name |
String | Repository name (e.g. my-repo) |
repo_owner |
String | Repository owner (e.g. eclipse-foo) |
repo_owner_id |
String | GitHub numeric owner ID |
Each Jenkins workload has a distinct issuer URL.
| Column | Type | Notes |
|---|---|---|
id |
FK → workloads.id | |
issuer |
String | OIDC issuer URL (e.g. https://ci.eclipse.org/<name>/oidc) |
A DependencyTrack target for SBOM uploads.
| Column | Type | Notes |
|---|---|---|
id |
Serial PK | |
ef_project_id |
FK → eclipse_foundation_projects.id | |
name |
String | DependencyTrack project name (matched against product_name in upload payload) |
parent_uuid |
String | DependencyTrack parent project UUID |
The Eclipse Foundation project id is the authorization boundary. Any workload can upload SBOMs for any DependencyTrack project that shares the same Eclipse Foundation project id.
- Web Framework: FastAPI (modern, has built-in model validation and API documentation)
- Database: PostgreSQL with SQLAlchemy ORM,
psycopg2, and Alembic for schema migrations - HTTP Client:
requestsfor OIDC discovery and DependencyTrack API calls - JWT Library:
PyJWTwithcryptographysupport- Handles token parsing, validation, and signature verification
- Includes
PyJWKClientfor JWKS key fetching
- CLI:
click - Testing:
pytest - Linting:
ruff - Python Project Management:
uv
main.py: FastAPI app with:- Application settings management
- Upload SBOM API endpoint implementing full authentication and DependencyTrack upload flow (section 3.1, items 4. through 6.)
models.py: Data models:- SQLAlchemy declarative
Base - SQLAlchemy ORM models:
EclipseFoundationProject,Workload,GitHubWorkload,JenkinsWorkload,DependencyTrackProject(see section 4.3) - Pydantic request models:
PiaUploadPayload,DependencyTrackUploadPayload
- SQLAlchemy declarative
oidc.py: OIDC token validation and signature verification using PyJWTdependencytrack.py: DependencyTrack API upload client for SBOMscli.py: Management CLI for registering Workloads and DependencyTrackProjects (see section 5.5)
-
Settings Errors: Fail fast at startup with clear error
-
Authentication Errors:
- 422 for malformed POST data
- 401 for unknown issuer, token verification errors, no matching workload
found for verified token claims, or no matching DependencyTrack project
for the workload's
ef_project_id
-
DependencyTrack Upload Errors: 502, if upload fails with an error, or HTTP status code from DependencyTrack
Log important events:
- Settings re-load
- Client connects
- Token verifications
- DependencyTrack uploads
- Errors
Metrics to track:
- Client connect attempts by IP
- Client connect attempts by
product_nameandproduct_version - Token verification time
- DependencyTrack upload time
- Total API response time
PIA includes a management CLI (pia) for registering workloads and
DependencyTrack projects. It is installed as a console entry point via
pyproject.toml and connects to the DB using the PIA_DATABASE_URL
environment variable, e.g. postgresql://user:secret@1.2.3.4:5432/pia.
Registers a new workload for an Eclipse Foundation project. The workload type is determined by the URL:
- GitHub (URL contains
github.com): Parsesownerandrepofrom the URL path. Queries the GitHub API (GET https://api.github.com/users/{owner}) to fetch the numericrepo_owner_id. Creates aGitHubWorkload. - Jenkins (URL starts with
https://ci.eclipse.org): Uses the URL asissuer. Creates aJenkinsWorkload.
If the EclipseFoundationProject row for ef_project_id does not exist, it is
created automatically.
Registers a DependencyTrack project for an Eclipse Foundation project. Looks up the project in DependencyTrack by name hierarchy:
- Find exactly one root project by
parent_name. - Find exactly one child project by
project_name - Store
DependencyTrackProject(name=project_name, parent_uuid=<child project UUID>)
The dt_url argument is the DependencyTrack base URL (e.g.
https://sbom.eclipse.org). Authentication uses the PIA_DEPENDENCY_TRACK_API_KEY
environment variable. The API key requires at least the VIEW_PORTFOLIO permission.
If the EclipseFoundationProject row for ef_project_id does not exist, it is
created automatically.
- Issuer and Project: Only trust allowed issuers and projects
- Expiration: Reject expired tokens
- Signature: Verify signature before trusting claims
- Algorithm: Only accept RS256 (Identity Provider restriction)
- Key Rotation: Refresh OIDC configs and JWKs
| Attack | Mitigation |
|---|---|
| Token replay | Short-lived tokens (5-10 min), do not re-use token (future work) |
| Issuer spoofing | HTTPS-only issuer URLs, strict URL validation |
| Token forgery | Cryptographic signature verification |
| Project impersonation | Claim-to-workload validation, project-scoped DependencyTrack project resolution |
| DoS via OIDC discovery | Cache OIDC discovery, rate limit requests (both future work) |
| Parsing vulnerabilities | Use minimal schema for unauthenticated POST data, use well-tested JWT library for token parsing |
- Store secrets securely in environment variables
- Never log tokens or credentials
- Rotate DependencyTrack credentials regularly
- All communication must use HTTPS:
- PIA API endpoint
- OIDC discovery URLs
- DependencyTrack API endpoint
Identity Provider:
- Issuer:
https://token.actions.githubusercontent.com - OIDC config path:
/.well-known/openid-configuration
Verified Claims:
repository_owner_id: GitHub numeric owner IDrepository: In the form{repo_owner}/{repo_name}
Example Publisher workflow:
jobs:
publish:
permissions:
id-token: write
steps:
- name: Get ID token
id: token
run: |
ID_TOKEN=$(curl \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pia.eclipse.org")
echo "ID_TOKEN=$ID_TOKEN" >> $GITHUB_OUTPUT
- name: Publish SBOM
run: |
curl -X POST https://pia.eclipse.org/v1/upload/sbom \
-H "Authorization: Bearer ${{ steps.token.outputs.ID_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"product_name": "...", "product_version": "...", "bom": "..."}'Identity Providers (one per project):
- Issuer:
https://ci.eclipse.org/<PROJECT NAME>/oidc - OIDC config path:
/.well-known/openid-configuration
Verified Claims:
iss: The issuer URL identifies the project (e.g.https://ci.eclipse.org/eclipse-baz/oidcidentifies project Eclipse Baz). No additional claims required.
Publisher Setup:
- Install plugins:
oidc-provider,credentials-binding - Configure per-project Jenkins instance URL
- Create
id-tokencredential- Set
GLOBALscope, to make tokens available to jobs - Set audience, to prevent cross-service token replay
- Set
Example Publisher Credential Setup:
credentials:
system:
domainCredentials:
- credentials:
- idToken:
id: id-token
scope: GLOBAL
audience: pia.eclipse.orgExample Publisher Pipeline:
pipeline {
agent any
environment {
ID_TOKEN = credentials('id-token')
}
stages {
stage('Publish') {
steps {
sh '''
curl -X POST https://pia.eclipse.org/v1/upload/sbom \
-H "Authorization: Bearer ${ID_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"product_name": "...", "product_version": "...", "bom": "..."}'
'''
}
}
}
}- Python 3.13.2+
- Production ASGI server (e.g., uvicorn)
- Reverse proxy for TLS termination and rate limiting
- Container deployment
Settings via:
- PostgreSQL database for Eclipse Foundation projects, workloads, and DependencyTrack projects (see section 4.3)
- Environment variables (with
PIA_prefix) for application settings
Required environment variables:
PIA_DEPENDENCY_TRACK_API_KEY=<your-api-key>
PIA_DATABASE_URL=postgresql://user:pass@host:5432/pia
PIA_DEPENDENCY_TRACK_URL=https://sbom.eclipse.org/api/v1/bom # optional, has default
PIA_EXPECTED_AUDIENCE=pia.eclipse.org # optional, has default- Stateless: Can run multiple instances behind load balancer
- Rate limiting per project/IP at reverse proxy level, if needed
- Async processing, if needed
- Settings loading and validation (pydantic-settings)
- Model validation (pydantic models)
- Token verification and authentication logic with test tokens
- Error handling paths
- FastAPI endpoints using httpx async test client
- DependencyTrack upload with mock server
- Add caching for oidc config and jwks
- Add monitoring
- Make upload API async, if sync takes too long
- Protect against replayed tokens (PyPI uses "jti" claim to track used tokens)
- Consider product-level authentication
- Trusted Publishing (PyPI)
- GitHub OIDC Documentation
- Jenkins OIDC Provider Plugin
- PyJWT Documentation
- RFC6750 - OAuth 2.0 Bearer Token Usage
- End-to-end Proof of Concept
- Concept Document (restricted access)
{
"jti": "...",
"sub": "repo:octo-org/octo-repo:ref:refs/heads/main",
"aud": "pia.eclipse.org",
"ref": "refs/heads/main",
"sha": "...",
"repository": "octo-org/octo-repo",
"repository_owner": "octo-org",
"run_id": "...",
"run_number": "...",
"run_attempt": "...",
"actor": "octocat",
"workflow": "CI",
"head_ref": "",
"base_ref": "",
"event_name": "push",
"ref_type": "branch",
"job_workflow_ref": "octo-org/octo-repo/.github/workflows/ci.yml@refs/heads/main",
"iss": "https://token.actions.githubusercontent.com",
"nbf": 1632492000,
"exp": 1632492900,
"iat": 1632492000
}{
"aud": "pia.eclipse.org",
"build_number": 2,
"exp": 1765461015,
"iat": 1765457415,
"iss": "https://ci.eclipse.org/my-project/oidc",
"sub": "https://ci.eclipse.org/my-project/job/oidc-upload-demo/"
}