rusty_paseto 0.10.0

A type-driven, ergonomic alternative to JWT for secure stateless PASETO tokens.
Documentation
export const metadata = {
  title: 'Expiration',
  description:
    'Learn how to handle token expiration and create non-expiring tokens when needed.',
}

# Expiration

Token expiration is a critical security feature. rusty_paseto provides automatic expiration validation and flexible options for setting token lifetimes. {{ className: 'lead' }}

## Setting Expiration

Set token expiration with an RFC3339 timestamp:

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

let token = PasetoBuilder::<V4, Local>::default()
    .set_claim(ExpirationClaim::try_from("2025-06-15T12:00:00Z")?)
    .build(&key)?;
```

### Using the Fluent Method

```rust
let token = PasetoBuilder::<V4, Local>::default()
    .expiration("2025-06-15T12:00:00Z")?
    .build(&key)?;
```

---

## Dynamic Expiration

Calculate expiration based on current time:

```rust
use rusty_paseto::prelude::*;
use time::{Duration, OffsetDateTime};

// Token expires in 1 hour
let exp = OffsetDateTime::now_utc() + Duration::hours(1);
let exp_str = exp.format(&time::format_description::well_known::Rfc3339)?;

let token = PasetoBuilder::<V4, Local>::default()
    .set_claim(ExpirationClaim::try_from(exp_str.as_str())?)
    .build(&key)?;
```

### Common Expiration Durations

```rust
use time::Duration;

// Short-lived tokens (API requests)
let exp = OffsetDateTime::now_utc() + Duration::minutes(15);

// Session tokens
let exp = OffsetDateTime::now_utc() + Duration::hours(24);

// Refresh tokens
let exp = OffsetDateTime::now_utc() + Duration::days(30);

// Long-lived API keys
let exp = OffsetDateTime::now_utc() + Duration::days(365);
```

---

## Automatic Validation

`PasetoParser` automatically validates expiration:

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

// This will fail if the token is expired
let result = PasetoParser::<V4, Local>::default()
    .parse(&token, &key);

match result {
    Ok(payload) => println!("Token valid: {}", payload),
    Err(Error::Expired) => println!("Token has expired!"),
    Err(e) => println!("Other error: {}", e),
}
```

---

## Not Before (nbf)

Use `NotBeforeClaim` for tokens that shouldn't be used until a specific time:

```rust
use rusty_paseto::prelude::*;
use time::{Duration, OffsetDateTime};

// Token valid starting in 1 hour, expires in 2 hours
let nbf = OffsetDateTime::now_utc() + Duration::hours(1);
let exp = OffsetDateTime::now_utc() + Duration::hours(2);

let nbf_str = nbf.format(&time::format_description::well_known::Rfc3339)?;
let exp_str = exp.format(&time::format_description::well_known::Rfc3339)?;

let token = PasetoBuilder::<V4, Local>::default()
    .set_claim(NotBeforeClaim::try_from(nbf_str.as_str())?)
    .set_claim(ExpirationClaim::try_from(exp_str.as_str())?)
    .build(&key)?;
```

Attempting to use the token before `nbf` returns `Error::NotYetValid`.

---

## Non-Expiring Tokens

<Note>
  Non-expiring tokens are a security risk. Only use them when you have other
  mechanisms for token rotation or revocation.
</Note>

To create a token without expiration:

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

let token = PasetoBuilder::<V4, Local>::default()
    .set_claim(SubjectClaim::from("service-account"))
    .set_no_expiration_danger_acknowledged()
    .build(&key)?;
```

### When Non-Expiring Tokens Are Acceptable

- Service-to-service authentication with key rotation
- Tokens stored in secure vaults with rotation policies
- Development/testing environments

### Better Alternatives

Instead of non-expiring tokens, consider:

1. **Long expiration with refresh:** 30-day tokens with a refresh mechanism
2. **Key rotation:** Rotate signing keys periodically
3. **Revocation lists:** Maintain a list of revoked token IDs

---

## Token Refresh Pattern

Implement a refresh token pattern for long sessions:

<Row>
  <Col>

    ```rust
    use rusty_paseto::prelude::*;
    use time::{Duration, OffsetDateTime};

    fn create_token_pair(
        user_id: &str,
        key: &PasetoSymmetricKey<V4, Local>,
    ) -> Result<(String, String), Box<dyn std::error::Error>> {
        let now = OffsetDateTime::now_utc();

        // Access token: short-lived (15 min)
        let access_exp = (now + Duration::minutes(15))
            .format(&time::format_description::well_known::Rfc3339)?;

        let access_token = PasetoBuilder::<V4, Local>::default()
            .subject(user_id)
            .set_claim(CustomClaim::new("type", "access")?)
            .expiration(&access_exp)?
            .build(key)?;

        // Refresh token: longer-lived (7 days)
        let refresh_exp = (now + Duration::days(7))
            .format(&time::format_description::well_known::Rfc3339)?;

        let refresh_token = PasetoBuilder::<V4, Local>::default()
            .subject(user_id)
            .set_claim(CustomClaim::new("type", "refresh")?)
            .expiration(&refresh_exp)?
            .build(key)?;

        Ok((access_token, refresh_token))
    }
    ```

  </Col>
</Row>