Skip to content

[Feature]: Support parent-child token relationships with claim inheritance #37

Description

@alexlovelltroy

Describe the feature

Implement token hierarchy where child tokens (subtokens) inherit claims from parent tokens, including MFA claims, for granular authorization with session-level MFA enforcement.

This feature enables:

  • Session tokens that spawn subtokens with inherited MFA claims
  • Revocation cascade (revoking parent revokes all children)
  • Claim degradation rules (children cannot elevate claims)

Current Behavior

  • tokensmith issues independent tokens with no relationships
  • No concept of token hierarchy or claim inheritance
  • Revoking a token doesn't affect related tokens

Desired Behavior

Token Hierarchy Model

Session Token (12hr lifetime, AMR: ["pwd", "otp"], ACR: "urn:mfa:required")
  ├─ API Token 1 (30 days, inherits AMR/ACR, scopes: ["read"])
  ├─ API Token 2 (60 days, inherits AMR/ACR, scopes: ["read", "write"])
  └─ Short-lived Token (5 min, inherits AMR/ACR, scopes: ["admin"])

Key Properties:

  • Parent tokens are typically session tokens (created via session management API)
  • Child tokens inherit MFA claims (amr, acr, auth_time) from parent
  • Revocation cascade: Revoking parent invalidates all descendants
  • Claim degradation: Children can only subset/restrict claims, never elevate

1. Create Child Token with Claim Inheritance

Endpoint: POST /oauth/token (extend existing endpoint from Issue #33)

Request Body (JSON):

{
  "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
  "subject_token": "{parent-jwt}",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "parent_token_id": "tok_parent_abc123",  // NEW: Explicit parent linkage
  "scope": "read",  // Child scopes (subset of parent)
  "expires_in": 2592000  // 30 days
}

Response (201 Created):

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRodW1icHJpbnQifQ...",
  "token_type": "Bearer",
  "expires_in": 2592000,
  "scope": "read",
  
  "token_id": "tok_child_def456",
  "parent_token_id": "tok_parent_abc123",
  
  // Inherited MFA claims (read-only, from parent)
  "amr": ["pwd", "otp"],
  "acr": "urn:mfa:required",
  "auth_time": 1719708600,
  "session_id": "sess_xyz789"
}

JWT Claims (Child Token):

{
  "sub": "user-123",
  "token_id": "tok_child_def456",
  "parent_id": "tok_parent_abc123",  // NEW: Track parent
  
  // Inherited from parent (immutable)
  "amr": ["pwd", "otp"],
  "acr": "urn:mfa:required",
  "auth_time": 1719708600,
  "session_id": "sess_xyz789",
  
  // Child-specific
  "scopes": ["read"],  // Subset of parent's scopes
  "exp": 1722300600,
  "iat": 1719708600
}

2. Claim Inheritance Rules

Allowed: Claim Degradation

Children can restrict claims:

// Parent
{
  "amr": ["pwd", "otp"],
  "scopes": ["read", "write", "delete"]
}

// Child (VALID)
{
  "amr": ["pwd", "otp"],  // Inherited as-is
  "scopes": ["read"]      // Subset of parent scopes
}

Forbidden: Claim Elevation

Children cannot add claims:

// Parent
{
  "amr": ["pwd"],
  "scopes": ["read"]
}

// Child (INVALID - validation error)
{
  "amr": ["pwd", "otp"],       // ❌ CANNOT add new auth methods
  "scopes": ["read", "write"]  // ❌ CANNOT add new scopes
}

Validation Error Response:

{
  "error": "invalid_request",
  "error_description": "Child token cannot elevate claims beyond parent. Parent AMR: [pwd], requested AMR: [pwd, otp]"
}

3. Revocation Cascade

Endpoint: DELETE /oauth/token/{token_id} (extend existing endpoint)

Behavior:

  1. Revoke the target token (add to invalidation list)
  2. Find all child tokens recursively
  3. Revoke each child token
  4. Return count of revoked tokens

Response (200 OK):

{
  "revoked_token_id": "tok_parent_abc123",
  "revoked_children": [
    "tok_child_def456",
    "tok_child_ghi789",
    "tok_grandchild_jkl012"
  ],
  "total_revoked": 4
}

Implementation:

func (s *TokenService) RevokeToken(ctx context.Context, tokenID string) error {
    // Revoke the token
    s.invalidateToken(tokenID)
    
    // Find and revoke all children (recursive)
    children := s.findChildTokens(tokenID)
    for _, child := range children {
        s.RevokeToken(ctx, child.ID)  // Recursive call
    }
    
    return nil
}

4. Token Hierarchy Query

Endpoint: GET /oauth/token/{token_id}/children

Authentication: Requires valid TokenSmith JWT

Authorization: User can view their own token hierarchies OR admin can view any

Response (200 OK):

{
  "token_id": "tok_parent_abc123",
  "children": [
    {
      "token_id": "tok_child_def456",
      "name": "api-key-1",
      "scopes": ["read"],
      "created_at": "2026-06-29T14:30:00Z",
      "expires_at": "2026-07-29T14:30:00Z",
      "revoked_at": null,
      "children_count": 0
    },
    {
      "token_id": "tok_child_ghi789",
      "name": "api-key-2",
      "scopes": ["read", "write"],
      "created_at": "2026-06-29T15:00:00Z",
      "expires_at": "2026-08-28T15:00:00Z",
      "revoked_at": null,
      "children_count": 1  // Has grandchildren
    }
  ]
}

Endpoint: GET /oauth/token/{token_id}/tree

Response (200 OK) - Full tree visualization:

{
  "token_id": "tok_parent_abc123",
  "name": "session-token",
  "type": "session",
  "amr": ["pwd", "otp"],
  "acr": "urn:mfa:required",
  "children": [
    {
      "token_id": "tok_child_def456",
      "name": "api-key-1",
      "type": "api_key",
      "amr": ["pwd", "otp"],  // Inherited
      "acr": "urn:mfa:required",  // Inherited
      "children": []
    },
    {
      "token_id": "tok_child_ghi789",
      "name": "api-key-2",
      "type": "api_key",
      "amr": ["pwd", "otp"],  // Inherited
      "acr": "urn:mfa:required",  // Inherited
      "children": [
        {
          "token_id": "tok_grandchild_jkl012",
          "name": "subtoken-1",
          "type": "subtoken",
          "amr": ["pwd", "otp"],  // Inherited
          "acr": "urn:mfa:required",  // Inherited
          "children": []
        }
      ]
    }
  ]
}

Why do you want this feature?

Use Cases

  1. Session-Based MFA Enforcement (primary use case):

    User authenticates with MFA → Session token captures AMR/ACR →
    User creates API keys (children) → API keys inherit MFA claims →
    Backend enforces "must have MFA" policies on API key usage
    
  2. Granular Revocation:

    • Revoke user session → all API keys created from that session are invalidated
    • Revoke API key → all subtokens derived from it are invalidated
    • Fine-grained control: revoke individual subtokens without affecting parent
  3. Audit Trail:

    • Track token lineage: "This API key was created during a session with 2FA"
    • Compliance: "All tokens in this hierarchy originated from an MFA-authenticated session"
  4. Scope Restriction:

    • Session token has full scopes
    • Create read-only API key (child) for CI/CD
    • Create admin API key (child) for operators
    • Each inherits MFA claims but with restricted scopes

Implementation Notes

Circular Reference Prevention

Validation at creation:

func (s *TokenService) CreateChildToken(req CreateTokenRequest) error {
    // Check for circular references
    if s.isDescendantOf(req.ParentTokenID, req.UserID) {
        return ErrCircularReference
    }
    
    // ... create token
}

func (s *TokenService) isDescendantOf(candidateParent, candidateChild string) bool {
    current := candidateParent
    for current != "" {
        if current == candidateChild {
            return true  // Circular reference detected
        }
        current = s.getParentID(current)
    }
    return false
}

Depth Limits

Prevent excessively deep hierarchies:

const MaxTokenDepth = 5  // Configurable

func (s *TokenService) validateHierarchyDepth(parentID string) error {
    depth := s.calculateDepth(parentID)
    if depth >= MaxTokenDepth {
        return ErrMaxDepthExceeded
    }
    return nil
}

Acceptance Criteria

  • POST /oauth/token accepts parent_token_id parameter
  • Child tokens inherit amr, acr, auth_time, session_id from parent
  • Validation prevents claim elevation (children cannot add new amr values or scopes)
  • Validation allows claim degradation (children can subset scopes)
  • DELETE /oauth/token/{id} revokes token and all descendants (cascade)
  • GET /oauth/token/{id}/children lists immediate children
  • GET /oauth/token/{id}/tree returns full hierarchy tree
  • Circular reference detection prevents invalid hierarchies
  • Depth limit prevents excessively deep hierarchies (default: 5 levels)
  • Integration tests for hierarchy creation, inheritance, and cascade revocation
  • Documentation with hierarchy examples and best practices

Dependencies

  • Session Management API (proposed in separate issue) - Session tokens are the typical parent tokens

Related Issues

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions