rusty_paseto 0.10.0

A type-driven, ergonomic alternative to JWT for secure stateless PASETO tokens.
Documentation
export const metadata = {
  title: 'Claim Validation',
  description:
    'Learn how to validate claims using check_claim, validate_claim, and custom validators.',
}

# Claim Validation

rusty_paseto provides flexible claim validation through exact matching, custom validators, and automatic time-based validation. {{ className: 'lead' }}

## Automatic Validation

`PasetoParser` automatically validates time-based claims:

<Properties>
  <Property name="exp (Expiration)">
    Tokens with past expiration times are rejected with `Error::Expired`
  </Property>
  <Property name="nbf (Not Before)">
    Tokens used before their not-before time are rejected with `Error::NotYetValid`
  </Property>
</Properties>

```rust
use rusty_paseto::prelude::*;

// Automatic validation happens during parse
let payload = PasetoParser::<V4, Local>::default()
    .parse(&token, &key)?;
// If exp is in the past -> Error::Expired
// If nbf is in the future -> Error::NotYetValid
```

---

## Exact Value Matching

Use `check_claim` to require exact claim values:

```rust
use rusty_paseto::prelude::*;

let payload = PasetoParser::<V4, Local>::default()
    .check_claim(AudienceClaim::from("api.myapp.com"))
    .check_claim(IssuerClaim::from("auth.myapp.com"))
    .check_claim(CustomClaim::new("role", "admin")?)
    .parse(&token, &key)?;
```

### Fluent Expect Methods

Cleaner syntax with `expect_*` methods:

```rust
let payload = PasetoParser::<V4, Local>::default()
    .expect_audience("api.myapp.com")
    .expect_issuer("auth.myapp.com")
    .expect_subject("user_123")
    .parse(&token, &key)?;
```

---

## Custom Validation Functions

Use `validate_claim` for complex validation logic:

```rust
use rusty_paseto::prelude::*;

let payload = PasetoParser::<V4, Local>::default()
    // Check role is one of allowed values
    .validate_claim("role", |value| {
        value.as_str()
            .map(|r| ["admin", "moderator", "user"].contains(&r))
            .unwrap_or(false)
    })
    // Check level is at least 5
    .validate_claim("level", |value| {
        value.as_i64()
            .map(|level| level >= 5)
            .unwrap_or(false)
    })
    // Check permissions array contains "read"
    .validate_claim("permissions", |value| {
        value.as_array()
            .map(|perms| perms.iter().any(|p| p == "read"))
            .unwrap_or(false)
    })
    .parse(&token, &key)?;
```

The validation function receives a `&serde_json::Value` and returns `bool`.

---

## Multiple Validators

Chain multiple validators for comprehensive validation:

```rust
use rusty_paseto::prelude::*;

let payload = PasetoParser::<V4, Local>::default()
    // Standard claims
    .expect_audience("api.myapp.com")
    .expect_issuer("auth.myapp.com")

    // Custom exact match
    .check_claim(CustomClaim::new("tenant", "acme-corp")?)

    // Custom validation
    .validate_claim("scope", |v| {
        v.as_str()
            .map(|s| s.split(' ').any(|scope| scope == "read:users"))
            .unwrap_or(false)
    })

    .parse(&token, &key)?;
```

---

## Validation Error Messages

When validation fails, you get descriptive errors:

```rust
use rusty_paseto::{prelude::*, Error};

let result = PasetoParser::<V4, Local>::default()
    .check_claim(AudienceClaim::from("expected-app"))
    .parse(&token, &key);

match result {
    Err(Error::InvalidClaimValue { claim, expected, actual }) => {
        println!("Claim '{}' failed: expected '{}', got '{}'",
                 claim, expected, actual);
        // Output: Claim 'aud' failed: expected 'expected-app', got 'other-app'
    }
    Err(Error::CustomValidationFailed(claim)) => {
        println!("Custom validation failed for claim: {}", claim);
    }
    _ => {}
}
```

---

## Validation Patterns

### Multi-Audience Validation

Accept tokens for multiple audiences:

```rust
let valid_audiences = ["api.myapp.com", "internal.myapp.com"];

let payload = PasetoParser::<V4, Local>::default()
    .validate_claim("aud", |value| {
        value.as_str()
            .map(|aud| valid_audiences.contains(&aud))
            .unwrap_or(false)
    })
    .parse(&token, &key)?;
```

### Required vs Optional Claims

```rust
// Required claim - parser fails if missing or invalid
.check_claim(SubjectClaim::from("user_123"))

// Optional claim - only validate if present
.validate_claim("optional_field", |value| {
    // Return true if null (not present) or valid
    value.is_null() || value.as_str().is_some()
})
```

### Type-Safe Struct Validation

For complex validation, parse into a struct first:

```rust
use rusty_paseto::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct Claims {
    sub: String,
    role: String,
    level: i32,
}

let claims: Claims = PasetoParser::<V4, Local>::default()
    .parse_into(&token, &key)?;

// Now use Rust's type system for validation
if claims.level < 5 {
    return Err("Insufficient level".into());
}

if !["admin", "user"].contains(&claims.role.as_str()) {
    return Err("Invalid role".into());
}
```