atomic-lti 2.2.0

A collection of LTI related functionality
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# atomic-lti

`atomic-lti` is a Rust library that provides support for integrating with the LTI 1.3 and LTI Advantage standards provided by 1EdTech.

## Overview

This library provides core types, traits, and utilities for building LTI 1.3 compliant tools. It includes comprehensive store traits for managing platforms, registrations, OIDC state, keys, and JWTs.

## Supported LTI Advantage Specifications

The following LTI Advantage specifications are supported:

- Names and Roles Provisioning Service (NRPS)
- Assignment and Grade Services (AGS)
- Deep Linking 2.0

## Features

- Complete LTI 1.3 Core support
- Enhanced store traits with CRUD operations
- Platform and registration management
- OIDC state tracking with issuer support
- JWT validation and signing
- Type-safe error handling
- Async/await throughout

## Installation

To use `atomic-lti` in your Rust project, add the following to your `Cargo.toml` file:

```toml
[dependencies]
atomic-lti = "2.1.0"
```

## Enhanced Store Traits

### PlatformStore

The `PlatformStore` trait manages LMS platform configurations. It provides both legacy single-platform methods and modern CRUD operations for multi-platform scenarios.

#### Platform Data Structure

```rust
use atomic_lti::stores::platform_store::PlatformData;

let platform = PlatformData {
    issuer: "https://canvas.instructure.com".to_string(),
    name: Some("Canvas LMS".to_string()),
    jwks_url: "https://canvas.instructure.com/api/lti/security/jwks".to_string(),
    token_url: "https://canvas.instructure.com/login/oauth2/token".to_string(),
    oidc_url: "https://canvas.instructure.com/api/lti/authorize_redirect".to_string(),
};
```

#### CRUD Operations

```rust
use atomic_lti::stores::platform_store::PlatformStore;

// Create a new platform
let created = store.create(platform).await?;

// Find by issuer
let found = store.find_by_iss("https://canvas.instructure.com").await?;

// Update platform
platform.name = Some("Updated Canvas".to_string());
let updated = store.update(&platform.issuer, platform).await?;

// List all platforms
let all_platforms = store.list().await?;

// Delete platform
store.delete("https://canvas.instructure.com").await?;
```

#### Backward Compatible Methods

For single-platform scenarios, legacy methods are still supported:

```rust
let oidc_url = store.get_oidc_url().await?;
let jwks_url = store.get_jwk_server_url().await?;
let token_url = store.get_token_url().await?;
```

#### Implementation Example

```rust
use atomic_lti::stores::platform_store::{PlatformStore, PlatformData};
use atomic_lti::errors::PlatformError;
use async_trait::async_trait;

struct DBPlatformStore {
    pool: PgPool,
    issuer: String,
}

#[async_trait]
impl PlatformStore for DBPlatformStore {
    async fn get_oidc_url(&self) -> Result<String, PlatformError> {
        // Query database for platform config
        let platform = sqlx::query_as!(
            Platform,
            "SELECT * FROM lti_platforms WHERE issuer = $1",
            &self.issuer
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(platform.oidc_url)
    }

    async fn create(&self, platform: PlatformData) -> Result<PlatformData, PlatformError> {
        // Insert new platform into database
        sqlx::query!(
            "INSERT INTO lti_platforms (issuer, name, jwks_url, token_url, oidc_url)
             VALUES ($1, $2, $3, $4, $5)",
            platform.issuer,
            platform.name,
            platform.jwks_url,
            platform.token_url,
            platform.oidc_url
        )
        .execute(&self.pool)
        .await?;

        Ok(platform)
    }

    async fn find_by_iss(&self, issuer: &str) -> Result<Option<PlatformData>, PlatformError> {
        let result = sqlx::query_as!(
            Platform,
            "SELECT * FROM lti_platforms WHERE issuer = $1",
            issuer
        )
        .fetch_optional(&self.pool)
        .await?;

        Ok(result.map(|p| PlatformData {
            issuer: p.issuer,
            name: p.name,
            jwks_url: p.jwks_url,
            token_url: p.token_url,
            oidc_url: p.oidc_url,
        }))
    }

    // Implement other methods...
}
```

### RegistrationStore

The `RegistrationStore` trait manages LTI tool registrations, including OAuth2 credentials, deployment information, and tool capabilities.

#### Registration Data Structure

```rust
use atomic_lti::stores::registration_store::RegistrationData;
use serde_json::json;

let registration = RegistrationData {
    platform_id: 1,
    client_id: "abc123".to_string(),
    deployment_id: Some("deployment-1".to_string()),
    registration_config: json!({
        "client_name": "My LTI Tool",
        "redirect_uris": ["https://example.com/lti/launch"]
    }),
    registration_token: None,
    status: "active".to_string(),
    supported_placements: Some(json!(["course_navigation", "assignment_selection"])),
    supported_message_types: Some(json!(["LtiResourceLinkRequest", "LtiDeepLinkingRequest"])),
    capabilities: Some(json!({
        "can_create_line_items": true,
        "can_update_grades": true
    })),
};
```

#### Helper Methods

```rust
// Check if registration supports a placement
if registration.supports_placement("course_navigation") {
    println!("Course navigation is supported");
}

// Check if registration supports a message type
if registration.supports_message_type("LtiResourceLinkRequest") {
    println!("Resource link requests are supported");
}

// Get a specific capability
if let Some(value) = registration.get_capability("can_create_line_items") {
    println!("Can create line items: {}", value);
}
```

#### Store Operations

```rust
use atomic_lti::stores::registration_store::RegistrationStore;

// Create a new registration
let created = store.create(registration).await?;

// Find by client ID
let found = store.find_by_client_id("abc123").await?;

// Find by platform and client ID (useful for multi-tenant scenarios)
let found = store.find_by_platform_and_client(1, "abc123").await?;

// Update status
let updated = store.update_status("abc123", "revoked").await?;

// Update capabilities
let new_capabilities = json!({
    "can_create_line_items": true,
    "can_update_grades": true,
    "max_score": 100
});
let updated = store.update_capabilities("abc123", new_capabilities).await?;
```

### OIDCStateStore

The `OIDCStateStore` trait manages OIDC authentication state and nonce values. Enhanced with issuer tracking for multi-platform support.

```rust
use atomic_lti::stores::oidc_state_store::OIDCStateStore;

// Get state and nonce
let state = store.get_state().await;
let nonce = store.get_nonce().await;

// Get issuer (enhanced feature)
if let Some(issuer) = store.get_issuer().await {
    println!("State is for platform: {}", issuer);
}

// Check creation time
let created_at = store.get_created_at().await;

// Destroy state after use
store.destroy().await?;
```

The issuer field allows associating OIDC state with specific platforms, enabling proper state validation in multi-platform scenarios.

### KeyStore

The `KeyStore` trait manages RSA key pairs for JWT signing and verification.

```rust
use atomic_lti::stores::key_store::KeyStore;

// Get current key for signing
let (kid, private_key) = store.get_current_key().await?;

// Get multiple keys (for key rotation)
let keys = store.get_current_keys(5).await?;

// Get specific key by ID
let key = store.get_key("key-id-123").await?;
```

### JwtStore

The `JwtStore` trait handles JWT creation from LTI ID tokens.

```rust
use atomic_lti::stores::jwt_store::JwtStore;

// Build JWT from ID token
let jwt = store.build_jwt(&id_token).await?;
```

## JWT Utilities

The library provides utilities for encoding and decoding JWTs with automatic key management:

```rust
use atomic_lti::jwt::{encode_using_store, decode_using_store};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct MyClaims {
    sub: String,
    exp: i64,
}

// Encode claims to JWT
let claims = MyClaims {
    sub: "user123".to_string(),
    exp: 1234567890,
};
let jwt_string = encode_using_store(&claims, &key_store).await?;

// Decode JWT back to claims
let token_data = decode_using_store::<MyClaims>(&jwt_string, &key_store).await?;
println!("User: {}", token_data.claims.sub);
```

## Error Handling

The library provides comprehensive error types:

```rust
use atomic_lti::errors::*;

// Platform errors
PlatformError::InvalidIss(String)
PlatformError::NotFound(String)

// Registration errors
RegistrationError::NotFound(String)
RegistrationError::AlreadyExists(String)

// OIDC errors
OIDCError::InvalidState
OIDCError::ExpiredState

// Security errors
SecureError::InvalidKeyId
SecureError::EmptyKeys
SecureError::JwtError(String)
```

## Migration from Previous Versions

### PlatformStore Changes

**Before (single platform):**
```rust
async fn get_oidc_url(&self) -> Result<String, PlatformError>;
```

**After (multi-platform):**
```rust
// Legacy method still works
async fn get_oidc_url(&self) -> Result<String, PlatformError>;

// New CRUD methods
async fn create(&self, platform: PlatformData) -> Result<PlatformData, PlatformError>;
async fn find_by_iss(&self, issuer: &str) -> Result<Option<PlatformData>, PlatformError>;
async fn update(&self, issuer: &str, platform: PlatformData) -> Result<PlatformData, PlatformError>;
async fn delete(&self, issuer: &str) -> Result<(), PlatformError>;
async fn list(&self) -> Result<Vec<PlatformData>, PlatformError>;
```

### OIDCStateStore Changes

**Enhanced with issuer tracking:**

```rust
// New method
async fn get_issuer(&self) -> Option<String>;
```

All existing methods remain backward compatible.

### New RegistrationStore

The `RegistrationStore` trait is entirely new and provides registration management capabilities.

## Best Practices

### 1. Multi-Platform Support

When supporting multiple LMS platforms, use the enhanced CRUD methods:

```rust
// List all configured platforms
let platforms = platform_store.list().await?;

for platform in platforms {
    println!("Platform: {} ({})", platform.name.unwrap_or_default(), platform.issuer);
}
```

### 2. Registration Management

Store registration data with all relevant fields for proper tool configuration:

```rust
let registration = RegistrationData {
    platform_id: platform.id,
    client_id: registration_response.client_id,
    deployment_id: Some(registration_response.deployment_id),
    registration_config: serde_json::to_value(&registration_response)?,
    status: "active".to_string(),
    supported_placements: Some(json!(placements)),
    supported_message_types: Some(json!(message_types)),
    capabilities: Some(json!(capabilities)),
};

registration_store.create(registration).await?;
```

### 3. State Cleanup

Always clean up OIDC states after use to prevent database bloat:

```rust
async fn handle_oidc_callback(state: &str, store: &impl OIDCStateStore) -> Result<(), Error> {
    // Validate state
    let state_obj = store.get_state().await;

    // ... process callback

    // Clean up
    store.destroy().await?;

    Ok(())
}
```

### 4. Error Handling

Use pattern matching for comprehensive error handling:

```rust
match platform_store.find_by_iss(issuer).await {
    Ok(Some(platform)) => {
        // Platform found, proceed
    }
    Ok(None) => {
        // Platform not found, maybe create it
        platform_store.create(new_platform).await?;
    }
    Err(e) => {
        // Database or other error
        eprintln!("Error: {}", e);
    }
}
```

## Testing

The library includes comprehensive tests for all store traits:

```bash
cargo test -- --nocapture
```

For testing your implementations, use the in-memory test stores provided in the test modules, or create mock implementations.

## Examples

See the following projects for complete implementations:

- **atomic-decay** - SQLx-based implementation with PostgreSQL
- **atomic-oxide** - Diesel-based implementation with PostgreSQL

## Related Crates

- **atomic-lti-tool** - Tool-specific structures and dependency injection
- **atomic-lti-tool-axum** - Axum web framework integration
- **atomic-lti-test** - Testing utilities and mock implementations

## Run Tests

To run the tests for `atomic-lti`, use the following command:

```bash
cargo test -- --nocapture
```

## License

MIT