crab-jwt 0.1.0
Minimal JWT/JWKS library with RS256 support - Python bindings for Rust
License: MIT
Requires Python: >=3.8
Keywords: jwt, jwks, rsa, authentication, token, rust

# crab-jwt

A minimal, fast JWT/JWKS library for Python with RS256 support, powered by Rust.

[![PyPI](https://img.shields.io/pypi/v/crab-jwt.svg)](https://pypi.org/project/crab-jwt/)
[![Python](https://img.shields.io/pypi/pyversions/crab-jwt.svg)](https://pypi.org/project/crab-jwt/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- **Fast** - Core cryptography implemented in Rust
- **Secure** - Uses audited RSA implementation
- **Simple API** - Fluent builder pattern for creating tokens
- **JWKS Support** - Generate and parse JSON Web Key Sets
- **Type Hints** - Full type annotation support (PEP 561)

## Installation

```bash
pip install crab-jwt
```

## Quick Start

```python
from crab_jwt import KeyPair, JwtBuilder, verify

# Generate RSA key pair
keys = KeyPair.generate()

# Create a signed JWT
token = (JwtBuilder()
    .subject("user123")
    .issuer("my-app")
    .claim("role", "admin")
    .expiration_in(3600)  # 1 hour
    .sign(keys))

# Verify and decode the token
claims = verify(token, keys.to_public_pem())
print(claims.sub)  # "user123"
print(claims.get("role"))  # "admin"
```

## Usage

### Creating Tokens

Use the builder pattern to construct JWT claims:

```python
from crab_jwt import KeyPair, JwtBuilder

keys = KeyPair.generate()

token = (JwtBuilder()
    .subject("user-id-123")              # sub claim
    .issuer("https://auth.example.com")  # iss claim
    .audience("https://api.example.com") # aud claim
    .expiration_in(3600)                 # exp: now + 1 hour
    .issued_now()                        # iat: current timestamp
    .not_before(1234567890)              # nbf claim
    .jwt_id("unique-token-id")           # jti claim
    .claim("role", "admin")              # custom claim
    .claim("permissions", ["read", "write"])  # custom claim
    .sign(keys))
```

### Verifying Tokens

```python
from crab_jwt import verify

claims = verify(token, public_pem)

# Access standard claims
print(claims.sub)  # Subject
print(claims.iss)  # Issuer
print(claims.exp)  # Expiration timestamp

# Access custom claims
role = claims.get("role")
permissions = claims.get("permissions")

# Get all custom claims as dict
custom = claims.custom_claims()
```

### Time Validation

Time validation is opt-in:

```python
claims = verify(token, public_pem)

# Check if token has expired
if claims.is_expired():
    raise Exception("Token has expired")

# Check both exp and nbf
if not claims.is_valid_time():
    raise Exception("Token is not valid at this time")
```

### Key Management

**Generate new keys:**

```python
from crab_jwt import KeyPair

keys = KeyPair.generate()
```

**Export to PEM:**

```python
private_pem = keys.to_private_pem()
public_pem = keys.to_public_pem()

# Save to files
with open("private.pem", "w") as f:
    f.write(private_pem)
```

**Import from PEM:**

```python
with open("private.pem") as f:
    keys = KeyPair.from_private_pem(f.read())
```

### JWKS (JSON Web Key Set)

Generate JWKS for public key distribution:

```python
from crab_jwt import Jwks, KeyPair

keys = KeyPair.generate()

# Create JWKS
jwks = Jwks.from_public_key(keys.to_public_pem(), "key-2024-01")

# Serialize to JSON (for your /.well-known/jwks.json endpoint)
json_str = jwks.to_json()
```

**Multiple keys (for key rotation):**

```python
jwks = Jwks()
jwks.add_key(current_key.to_public_pem(), "key-2024-02")
jwks.add_key(previous_key.to_public_pem(), "key-2024-01")

print(jwks.key_ids())  # ["key-2024-02", "key-2024-01"]
```

**Verify with JWKS:**

```python
from crab_jwt import Jwks, verify_with_jwks

# Parse JWKS (e.g., fetched from HTTP)
jwks = Jwks.from_json(jwks_json)

# Verify token using key ID
claims = verify_with_jwks(token, jwks, "key-2024-01")
```

## API Reference

### `KeyPair`

| Method | Description |
|--------|-------------|
| `KeyPair.generate()` | Generate new 2048-bit RSA key pair |
| `KeyPair.from_private_pem(pem)` | Import from PEM string |
| `.to_private_pem()` | Export private key to PEM |
| `.to_public_pem()` | Export public key to PEM |

### `JwtBuilder`

| Method | Description |
|--------|-------------|
| `.subject(sub)` | Set subject claim |
| `.issuer(iss)` | Set issuer claim |
| `.audience(aud)` | Set audience claim |
| `.expiration(timestamp)` | Set expiration (Unix timestamp) |
| `.expiration_in(seconds)` | Set expiration relative to now |
| `.issued_at(timestamp)` | Set issued-at timestamp |
| `.issued_now()` | Set issued-at to current time |
| `.not_before(timestamp)` | Set not-before timestamp |
| `.jwt_id(jti)` | Set unique token ID |
| `.claim(key, value)` | Add custom claim |
| `.sign(keys)` | Sign and return token string |

### `Claims`

| Property/Method | Description |
|-----------------|-------------|
| `.sub` | Subject claim |
| `.iss` | Issuer claim |
| `.aud` | Audience claim |
| `.exp` | Expiration timestamp |
| `.iat` | Issued-at timestamp |
| `.nbf` | Not-before timestamp |
| `.jti` | JWT ID |
| `.is_expired()` | Check if token has expired |
| `.is_valid_time()` | Check exp and nbf validity |
| `.get(key)` | Get custom claim value |
| `.custom_claims()` | Get all custom claims as dict |

### `Jwks`

| Method | Description |
|--------|-------------|
| `Jwks()` | Create empty JWKS |
| `Jwks.from_public_key(pem, kid)` | Create JWKS with one key |
| `Jwks.from_json(json)` | Parse JWKS from JSON |
| `.add_key(pem, kid)` | Add a key to the set |
| `.key_ids()` | Get list of key IDs |
| `.to_json()` | Serialize to JSON |

### Functions

| Function | Description |
|----------|-------------|
| `verify(token, public_pem)` | Verify token with public key PEM |
| `verify_with_jwks(token, jwks, kid)` | Verify token using JWKS |

## Error Handling

All errors raise `ValueError` with descriptive messages:

```python
from crab_jwt import verify

try:
    claims = verify(token, public_pem)
except ValueError as e:
    if "invalid signature" in str(e):
        print("Signature verification failed")
    elif "invalid token" in str(e):
        print("Malformed token")
    else:
        print(f"Error: {e}")
```

## Performance

crab-jwt is powered by Rust, providing significant performance benefits over pure Python implementations for cryptographic operations.

## License

MIT License