Designing PIC-X: Exchanging an OAuth Access Token for an Initial PCA
This article defines the first PIC-X exchange flow: deriving an initial PIC Context of Authority (PCA) from a validated OAuth access token through an Exchange Profile.
- tags
- #Pic #Pic-X #Oauth #Token Exchange #Exchange Profile #Configuration #Security #Authorization #Software Engineering #Design
- published
- reading time
- 8 minutes

PIC-X receives an OAuth access token, validates it, and converts it into the initial PCA from which the PIC lineage begins.
OAuth access token
│
▼
Exchange Profile
├── token validation
├── claim normalization
├── scope parsing
└── PCA0 construction
│
▼
Initial PIC lineage
Developer Experience
Using PIC-X requires only two exchange operations.
Create the initial PCA from an OAuth access token:
pcaInitial = picX.exchange(
accessToken,
executionContract
)
Use the PCA during application authorization:
decision = applicationPdp.evaluate(...)
pcaNext = picX.exchange(
pcaInitial,
proofOfRequest
)
At a glance:
OAuth access token
│
▼
picX.exchange(accessToken, executionContract)
│
▼
PCA initial
│
├── application PDP evaluation
│
▼
picX.exchange(pca, proofOfRequest)
│
▼
PCA next
The application developer does not need to implement token validation, scope parsing, PCA construction, invariant selection, or lineage verification.
Info: The exchange can also be performed by an API gateway, service mesh, or another infrastructure component. In that model, PIC-X remains transparent to the application: the application receives the PCA and the authorization result without implementing the exchange flow.
Warning: The following example uses an application PDP through AuthZEN. It must not be confused with PIC Trusted Anchors such as Guardrail. Trusted Anchors are protocol-level trust policy engines: they evaluate granted scopes, verify signatures, and enforce non-repudiation and other PIC trust rules. They are part of the PIC protocol flow, not the application authorization flow shown here. Trusted Anchors and Guardrail will be described in a separate article.
The remaining sections explain how PIC-X performs the first exchange.
The exchange operation will be exposed through a PIC-specific OAuth Token Exchange Profile. The profile will define how an OAuth access token is exchanged for an initial PCA and will be developed in the next articles.
1. Incoming OAuth Access Token
This example uses a signed JWT access token.
{
"header": {
"alg": "ES256",
"kid": "idp-key-1",
"typ": "at+jwt"
},
"payload": {
"iss": "https://idp.example.com",
"sub": "user-123",
"aud": "pic-x",
"iat": 1785589400,
"exp": 1785590000,
"name": "Oliver Bennett",
"email": "oliver.bennett@example.com",
"tenant_id": "tenant-a",
"roles": [
"document-manager"
],
"groups": [
"document-management",
"eu-employees"
],
"scope": "documents:read documents:write documents:read:document-42"
}
}
PIC-X validates the token before using any claim.
2. Exchange Profile
When PIC-X starts, it loads one or more Exchange Profiles. Each profile defines how tokens from a specific identity provider are validated and mapped into PIC authority. Multiple profiles may therefore coexist, allowing PIC-X to integrate with different IdPs while exposing a single PIC-X exchange interface.
An Exchange Profile defines how the scopes issued by its identity provider are converted into PIC privileges.
In this simple example, two scope formats are supported:
<resource-type>:<operation>
<resource-type>:<operation>:<resource-id>
For example:
documents:read
documents:write
documents:read:document-42
Below is the Exchange Profile configuration for a specific identity provider:
exchangeProfile:
id: corporate-oauth-to-pic
source:
tokenType: oauth-access-token
format: jwt
issuer: https://idp.example.com
audience: pic-x
validation:
allowedAlgorithms:
- ES256
requireExpiration: true
requireTokenType: at+jwt
claims:
principal:
id:
from: sub
roles:
from: roles
type: set
groups:
from: groups
type: set
attributes:
securityDomain:
from: tenant_id
scopes:
from: scope
encoding: space-delimited
type: set
privileges:
source: scopes
rules:
- name: resource-instance
priority: 10
pattern: '^(?<resourceType>[a-z][a-z0-9_-]*):(?<operation>[a-z][a-z0-9_-]*):(?<resourceId>[a-zA-Z0-9_-]+)$'
emit:
scope: '${raw}'
operation: '${operation}'
resourceType: '${resourceType}'
resourceId: '${resourceId}'
- name: resource-collection
priority: 1
pattern: '^(?<resourceType>[a-z][a-z0-9_-]*):(?<operation>[a-z][a-z0-9_-]*)$'
emit:
scope: '${raw}'
operation: '${operation}'
resourceType: '${resourceType}'
resourceId: '*'
onUnmatchedScope: reject
raw is the original scope string matched by the rule.
Rules are evaluated from priority 10 to priority 1.
10 = highest priority
1 = lowest priority
A more specific rule must have a higher priority than a generic rule. Otherwise, the collection rule could consume a resource-specific scope before the instance rule evaluates it.
documents:read:document-42
For this reason:
resource-instance → priority 10
resource-collection → priority 1
Rules with the same priority are evaluated in an unspecified order. Profiles should therefore avoid equal priorities when rules can match overlapping inputs.
raw = documents:write
The Exchange Profile validates and maps the access token. The execution contract is supplied separately as an input to picX.exchange.
3. Normalized Exchange Result
For the sample token, the Exchange Profile produces the following normalized result:
{
"principal": {
"id": "user-123",
"roles": [
"document-manager"
],
"groups": [
"document-management",
"eu-employees"
]
},
"attributes": {
"securityDomain": "tenant-a"
},
"privileges": [
{
"scope": "documents:read",
"operation": "read",
"resourceType": "documents",
"resourceId": "*"
},
{
"scope": "documents:write",
"operation": "write",
"resourceType": "documents",
"resourceId": "*"
},
{
"scope": "documents:read:document-42",
"operation": "read",
"resourceType": "documents",
"resourceId": "document-42"
}
]
}
Note:
principalis optional. A valid exchange result must containprincipal,privileges, or both.
Each privilege is one atomic authority item:
privilege = (scope, operation, resourceType, resourceId)
4. Initial PCA
Below is an example of an initial PCA:
{
"profile": "https://pic-protocol.org/0.1",
"issuer": "pic-x:corporate-oauth",
"principal": {
"id": "user-123",
"roles": [
"document-manager"
],
"groups": [
"document-management",
"eu-employees"
]
},
"attributes": {
"securityDomain": "tenant-a"
},
"execution": {
"invariants": [
{
"scope": "documents:read",
"operation": "read",
"resourceType": "documents",
"resourceId": "*"
},
{
"scope": "documents:write",
"operation": "write",
"resourceType": "documents",
"resourceId": "*"
},
{
"scope": "documents:read:document-42",
"operation": "read",
"resourceType": "documents",
"resourceId": "document-42"
}
],
"contract": {
"corporation": "acme",
"departments": [
"engineering",
"operations"
]
}
},
"continuation": {
"challenge": "base64url-random-256-bit-value",
"mode": "single-use",
"expiresAt": "2026-08-01T14:15:00Z"
},
"issuedAt": "2026-08-01T14:00:00Z",
"expiresAt": "2026-08-01T14:15:00Z"
}
Warning:
principalandattributesare optional. Either field may be omitted when the Exchange Profile does not produce it.
execution.invariants carries the authority that must be preserved or attenuated across the lineage.execution.contract carries execution-specific constraints.
5. Providing an Execution Contract
The execution contract is a mandatory input to the initial exchange:
pcaInitial = picX.exchange(
accessToken,
executionContract
)
It is provided by the caller, not by the Exchange Profile.
Example input:
{
"corporation": "acme",
"departments": [
"engineering",
"operations"
]
}
PIC-X places the supplied value in the initial PCA:
{
"execution": {
"contract": {
"corporation": "acme",
"departments": [
"engineering",
"operations"
]
}
}
}
The contract must contain at least one attribute with a non-empty value.
Each attribute supports only one of these value types:
non-empty string
non-empty array of non-empty strings
Valid examples:
{
"corporation": "acme"
}
{
"departments": [
"engineering",
"operations"
]
}
{
"corporation": "acme",
"regions": [
"eu-west-1",
"eu-central-1"
]
}
Invalid examples:
{}
{
"corporation": ""
}
{
"departments": []
}
{
"retryCount": 3,
"enabled": true,
"limits": {
"cpu": "2"
}
}
Numbers, booleans, objects, null values, empty strings, empty arrays, and arrays containing empty or non-string values are not supported.
Warning:
execution.contractdoes not replaceprincipal,attributes, orexecution.invariants. It adds execution constraints to the authority already carried by the PCA.
6. Selecting Authority by Scope
The application declares the scope required for the operation:
requiredScope = "documents:write"
A developer might use code as simple as the following:
principal = selectPrincipal(pca)
privilege = selectInvariantByScope(
pca,
"documents:write"
)
Selected principal:
{
"id": "user-123",
"roles": [
"document-manager"
],
"groups": [
"document-management",
"eu-employees"
]
}
Selected privilege:
{
"scope": "documents:write",
"operation": "write",
"resourceType": "documents",
"resourceId": "*"
}
The AuthZEN values are derived from the selected values:
subject = toAuthZenSubject(principal)
action = selectOperation(privilege)
resource = selectResource(privilege)
Derived action:
{
"name": "write"
}
Derived resource:
{
"type": "documents",
"id": "*"
}
7. Mapping to AuthZEN
Once the authority has selected the privilege, the application can invoke the PDP interface:
principal = selectPrincipal(pca)
privilege = selectInvariantByScope(
pca,
"documents:write"
)
subject = toAuthZenSubject(principal)
action = selectOperation(privilege)
resource = selectResource(privilege)
decision = pdp.authzen.evaluate(
subject,
action,
resource,
{
scope: privilege.scope,
securityDomain: pca.attributes.securityDomain
}
)
Here a sample AuthZEN request:
POST /access/v1/evaluation HTTP/1.1
Host: pdp.example.com
Authorization: Bearer <pdp-client-token>
Content-Type: application/json
X-Request-ID: bfe9eb29-ab87-4ca3-be83-a1d5d8305716
{
"subject": {
"type": "user",
"id": "user-123",
"properties": {
"roles": [
"document-manager"
],
"groups": [
"document-management",
"eu-employees"
]
}
},
"resource": {
"type": "documents",
"id": "*",
"properties": {
"securityDomain": "tenant-a"
}
},
"action": {
"name": "write"
},
"context": {
"scope": "documents:write",
"securityDomain": "tenant-a"
}
}
Info: PIC does not depend on AuthZEN. The following section only shows one possible integration with an application PDP exposed through the AuthZEN interface.
The mapping is direct:
principal.id → subject.id
principal.roles → subject.properties.roles
principal.groups → subject.properties.groups
privilege.operation → action.name
privilege.resourceType → resource.type
privilege.resourceId → resource.id
privilege.scope → context.scope
securityDomain → context.securityDomain
8. Cedar Policy
The PDP receives a normal AuthZEN request.
Cedar evaluates the policy that matches the selected action and resource:
permit (
principal is User,
action == Action::"write",
resource is Document
)
when {
context.scope == "documents:write" &&
context.securityDomain == resource.securityDomain
};A policy may also use principal attributes:
permit (
principal is User,
action == Action::"write",
resource is Document
)
when {
context.scope == "documents:write" &&
context.securityDomain == resource.securityDomain &&
principal.roles.contains("document-manager")
};The application may require access to a specific resource:
requiredScope = "documents:read:document-42"
The corresponding execution invariant is selected:
{
"scope": "documents:read:document-42",
"operation": "read",
"resourceType": "documents",
"resourceId": "document-42"
}
The application PDP can evaluate a policy for that specific resource:
{
"action": {
"name": "read"
},
"resource": {
"type": "documents",
"id": "document-42"
},
"context": {
"scope": "documents:read:document-42",
"securityDomain": "tenant-a"
}
}
Here a sample Cedar policy:
permit (
principal is User,
action == Action::"read",
resource == Document::"document-42"
)
when {
context.scope == "documents:read:document-42"
};End-to-End Flow
OAuth JWT + executionContract
│
▼
picX.exchange(accessToken, executionContract)
│
▼
Exchange Profile
├── parses each scope
└── emits:
├── scope
├── operation
├── resourceType
└── resourceId
│
▼
PCA0
├── optional principal
├── optional attributes
├── execution invariants
└── mandatory execution contract
│
▼
Application
│
└── application authorization and picX.exchange(pca, proofOfRequest)
│
▼
PIC-X
├── selectPrincipal(pca)
└── selectInvariantByScope(pca, requiredScope)
│
▼
Selected invariant
├── operation
├── resourceType
└── resourceId
│
▼
AuthZEN adapter
├── principal → subject
├── operation → action
├── resourceType → resource.type
├── resourceId → resource.id
└── scope → context.scope
│
▼
Application PDP
└── evaluates application policy