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.

Designing PIC-X: Exchanging an OAuth Access Token for an Initial PCA.
Designing PIC-X. Exchanging an OAuth Access Token for an Initial PCA.

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: principal is optional. A valid exchange result must contain principal, 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: principal and attributes are 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.contract does not replace principal, attributes, or execution.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