Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 67 additions & 94 deletions content/stellar-contracts/accounts/authorization-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,30 @@
title: Authorization Flow
---

Authorization in smart accounts is determined by matching the current context against the account's context rules. Rules are gathered, ordered by recency, and evaluated until one satisfies the requirements. If a matching rule is found, its policies (if any) are enforced. Otherwise, authorization fails.
Authorization in smart accounts is determined by matching the current context against explicitly selected context rules. The caller supplies `context_rule_ids` in the `AuthPayload`, specifying exactly one rule per auth context. If the selected rule passes all checks, its policies (if any) are enforced. Otherwise, authorization fails.

## AuthPayload

The `AuthPayload` structure is passed as the signature data in `__check_auth`:

```rust
#[contracttype]
pub struct AuthPayload {
/// Signature data mapped to each signer.
pub signers: Map<Signer, Bytes>,
/// Per-context rule IDs, aligned by index with `auth_contexts`.
pub context_rule_ids: Vec<u32>,
}
```

Each entry in `context_rule_ids` specifies the rule ID to validate against for the corresponding auth context (by index). Its length must equal `auth_contexts.len()`.

<Callout type="warning">
The `context_rule_ids` are bound into the signed digest: `sha256(signature_payload || context_rule_ids.to_xdr())`. This prevents rule-selection downgrade attacks where an attacker could redirect a signature to a less restrictive rule.
</Callout>

## Detailed Flow

```mermaid
sequenceDiagram
participant User
Expand All @@ -14,105 +35,67 @@ sequenceDiagram
participant Verifier
participant Policy

User->>SmartAccount: Signatures
SmartAccount->>ContextRule: Match context<br/>(CallContract, Default, ...)
ContextRule->>ContextRule: Filter expired rules<br/>Sort newest first

loop Each rule until match
Note over ContextRule,DelegatedSigner: Built-in authorization <br/>for delegated signers
ContextRule->>DelegatedSigner: require_auth_for_args()
DelegatedSigner-->>ContextRule: Authorized
User->>SmartAccount: AuthPayload (signers + context_rule_ids)
SmartAccount->>SmartAccount: Authenticate all provided signers

Note over ContextRule,Verifier: Signature verification for external signers
ContextRule->>Verifier: verify()
Verifier-->>ContextRule: Valid
loop Each (context, rule_id) pair
SmartAccount->>ContextRule: Look up rule by ID
ContextRule->>ContextRule: Reject if expired or<br/>context type mismatch

Note over ContextRule,Policy: Policy pre-checks
ContextRule->>Policy: can_enforce()
Policy-->>ContextRule: True/False
Note over ContextRule,DelegatedSigner: Identify authenticated signers<br/>from rule's signer list
Note over ContextRule,Verifier: External signers verified<br/>during initial authentication

alt All checks pass
alt Rule has policies
ContextRule->>Policy: enforce()
Policy->>Policy: Update state
ContextRule-->>SmartAccount: ✓ Authorized
else Any check fails
ContextRule->>ContextRule: Try next rule
Policy->>Policy: Validate + update state
Policy-->>ContextRule: Success (or panic)
else No policies
ContextRule->>ContextRule: All rule signers<br/>must be authenticated
end
end

SmartAccount-->>User: Success
SmartAccount-->>User: Success or Denied
```

### 1. Rule Collection
### 1. Signer Authentication

The smart account gathers all relevant context rules for evaluation:
All signers in the `AuthPayload` are authenticated upfront:

- Retrieve all non-expired rules for the specific context type
- Include default rules that apply to any context
- Sort specific and default rules by creation time (newest first)

**Context Type Matching:**
- For a `CallContract(address)` context, both specific `CallContract(address)` rules and `Default` rules are collected
- For a `CreateContract(wasm_hash)` context, both specific `CreateContract(wasm_hash)` rules and `Default` rules are collected
- For any other context, only `Default` rules are collected
- **Delegated Signer**: The address has authorized the operation via `require_auth_for_args(payload)`
- **External Signer**: The verifier contract confirms the signature is valid for the public key

**Expiration Filtering:**
Rules with `valid_until` set to a ledger sequence that has passed are automatically filtered out during collection.
Any signer in the `AuthPayload` that is not part of any selected context rule is rejected.

### 2. Rule Evaluation

For each rule in order (newest and most specific first):

#### Step 2.1: Signer Filtering
For each (context, rule_id) pair:

Extract authenticated signers from the rule's signer list. A signer is considered authenticated if:
#### Step 2.1: Rule Lookup and Validation

- **Delegated Signer**: The address has authorized the operation via `require_auth_for_args(payload)`
- **External Signer**: The verifier contract confirms the signature is valid for the public key
The rule is looked up by its explicit ID. The rule is rejected if:
- It does not exist
- It is expired (`valid_until` has passed)
- Its context type does not match the actual context (`Default` rules match any context)

Only authenticated signers proceed to the next step.
#### Step 2.2: Signer Matching

#### Step 2.2: Policy Validation

If the rule has attached policies, verify that all can be enforced:

```rust
for policy in rule.policies {
if !policy.can_enforce(e, account, rule_id, signers, auth_context) {
// This rule fails, try the next rule
}
}
```

If any policy's `can_enforce()` returns false, the rule fails and evaluation moves to the next rule.
Authenticated signers are identified from the rule's signer list.

#### Step 2.3: Authorization Check

The authorization check depends on whether policies are present:

**With Policies:**
- Success if all policies passed `can_enforce()`
- The presence of authenticated signers is verified during policy evaluation
- `enforce()` is called on each policy. If any `enforce()` panics, the authorization fails.
- Signer validation is deferred to the policies (e.g., threshold checks).

**Without Policies:**
- Success if all signers in the rule are authenticated
- At least one signer must be authenticated for the rule to match

#### Step 2.4: Rule Precedence

The first matching rule wins. Newer rules take precedence over older rules for the same context type. This allows overwriting old rules.
- All signers in the rule must be authenticated.
- At least one signer must be present.

### 3. Policy Enforcement

If authorization succeeds, the smart account calls `enforce()` on all matched policies in order:

```rust
for policy in matched_rule.policies {
policy.enforce(e, account, rule_id, signers, auth_context);
}
```

This triggers any necessary state changes such as updating spending counters, recording timestamps, emitting audit events, or modifying allowances.
Policy enforcement happens during rule evaluation. When `enforce()` is called, policies both validate conditions and perform state changes (updating spending counters, recording timestamps, emitting audit events, etc.).

Policy enforcement requires the smart account's authorization, ensuring that policies can only be enforced by the account itself.

Expand Down Expand Up @@ -148,19 +131,14 @@ ContextRule {

**Call Context:** `CallContract(dex_address)`

**Authorization Entries:** `[passkey_signature]`
**AuthPayload:** `{ signers: [passkey_signature], context_rule_ids: [2] }`

**Flow:**
1. Collect: Rules 2 (specific) and 1 (default)
2. Evaluate Rule 2:
- Signer filtering: Passkey authenticated
- Policy validation: Spending limit check passes
- Authorization check: All policies enforceable → Success
3. Enforce: Update spending counters, emit events
1. Authenticate: Passkey signature verified
2. Look up Rule 2: Not expired, context type matches
3. Enforce: Spending limit policy validates and updates counters
4. Result: Authorized

If the spending limit had been exceeded, Rule 2 would fail and evaluation would continue to Rule 1 (which would also fail since the passkey doesn't match Alice or Bob).

### Fallback to Default

**Configuration:**
Expand All @@ -185,16 +163,14 @@ ContextRule {

**Call Context:** `CallContract(dex_address)`

**Authorization Entries:** `[ed25519_alice_signature, ed25519_bob_signature]`
**AuthPayload:** `{ signers: [alice_sig, bob_sig], context_rule_ids: [1] }`

**Flow:**
1. Collect: Rule 2 filtered out (expired), only Rule 1 collected
2. Evaluate Rule 1: Both Alice and Bob authenticated → Success
3. Enforce: No policies to enforce
1. Authenticate: Alice and Bob signatures verified
2. Look up Rule 1: Default rule matches any context, not expired
3. No policies: Both Alice and Bob authenticated → Success
4. Result: Authorized

The expired session rule is automatically filtered out, and authorization falls back to the default admin rule.

### Authorization Failure

**Configuration:**
Expand All @@ -210,24 +186,21 @@ ContextRule {

**Call Context:** `CallContract(any_address)`

**Authorization Entries:** `[alice_signature]`
**AuthPayload:** `{ signers: [alice_signature], context_rule_ids: [1] }`

**Flow:**
1. Collect: Default rule retrieved
2. Evaluate:
- Signer filtering: Only Alice authenticated
- Policy validation: Threshold policy requires 2 signers, only 1 present → Fail
3. No more rules to evaluate
1. Authenticate: Alice signature verified
2. Look up Rule 1: Default rule, not expired
3. Enforce: Threshold policy requires 2 signers, only 1 present → panics
4. Result: Denied (transaction reverts)

## Performance Considerations

Protocol 23 optimizations make the authorization flow efficient:
- **Marginal storage read costs**: Reading multiple context rules has negligible cost
- **Marginal storage read costs**: Reading context rules has negligible cost
- **Cheaper cross-contract calls**: Calling verifiers and policies is substantially cheaper

The framework enforces limits to maintain predictability:
- Maximum context rules per smart account: 15
The framework enforces per-rule limits to maintain predictability:
- Maximum signers per context rule: 15
- Maximum policies per context rule: 5

Expand Down
23 changes: 12 additions & 11 deletions content/stellar-contracts/accounts/context-rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ List of authorized signers (maximum 15 per rule). Signers can be either delegate
For detailed documentation on signers, see [Signers](/stellar-contracts/accounts/signers-and-verifiers).

#### Policies
List of policy contracts (maximum 5 per rule). Policies act as enforcement modules that perform read-only prechecks and state-changing enforcement logic.
List of policy contracts (maximum 5 per rule). Policies act as enforcement modules that validate and enforce authorization constraints.

For detailed documentation on policies, see [Policies](/stellar-contracts/accounts/policies).

Expand All @@ -44,28 +44,29 @@ Each rule must contain at least one signer OR one policy. This enables pure poli
### Multiple Rules Per Context
Multiple rules can exist for the same context type with different signer sets and policies. This allows progressive authorization models where different combinations of credentials grant access to the same operations.

### Rule Precedence
Rules are evaluated in reverse chronological order (newest first). The first matching rule wins. This enables seamless permission updates: adding a new rule with different requirements immediately takes precedence over older rules for the same context.
### Explicit Rule Selection
The caller explicitly selects which rule to validate against for each auth context via `AuthPayload::context_rule_ids`. No automatic iteration or rule precedence is applied — the caller chooses the exact rule to use.

### Automatic Expiration
Expired rules are automatically filtered out during authorization evaluation.
Expired rules are rejected during authorization evaluation.

## Context Rule Limits

The framework enforces limits to keep costs predictable and encourage proactive context rule management (remove expired or non-valid rules):
The framework enforces per-rule limits to maintain predictability:

- Maximum context rules per smart account: 15
- Maximum signers per context rule: 15
- Maximum policies per context rule: 5

There is no upper limit on the total number of context rules per smart account.

## Authorization Matching

During authorization, the framework:
During authorization, the caller supplies `context_rule_ids` in the `AuthPayload`, one per auth context. For each (context, rule_id) pair:

1. Gathers all non-expired rules matching the context type plus default rules
2. Sorts rules by creation time (newest first)
3. Evaluates rules in order until one matches
4. Returns the first matching rule or fails if none match
1. The rule is looked up by its explicit ID
2. The rule is validated: must not be expired, context type must match
3. Signers are authenticated and policies enforced
4. Authorization succeeds if all checks pass, otherwise fails

For detailed documentation on the authorization flow, see [Authorization Flow](/stellar-contracts/accounts/authorization-flow).

Expand Down
Loading
Loading