+12
-5
CLAUDE.md
+12
-5
CLAUDE.md
···
23
#### Identity Management
24
- **Resolve identities**: `cargo run --features clap --bin atproto-identity-resolve -- <handle_or_did>`
25
- **Resolve with DID document**: `cargo run --features clap --bin atproto-identity-resolve -- --did-document <handle_or_did>`
26
-
- **Generate keys**: `cargo run --features clap --bin atproto-identity-key -- generate p256`
27
- **Sign data**: `cargo run --features clap --bin atproto-identity-sign -- <did_key> <json_file>`
28
- **Validate signatures**: `cargo run --features clap --bin atproto-identity-validate -- <did_key> <json_file> <signature>`
29
···
44
## Architecture
45
46
A comprehensive Rust workspace with multiple crates:
47
-
- **atproto-identity**: Core identity management with 8 modules (resolve, plc, web, model, validation, config, errors, key)
48
- **atproto-record**: Record signature operations and validation
49
- **atproto-client**: HTTP client with OAuth and identity integration
50
- **atproto-jetstream**: WebSocket event streaming with compression
51
-
- **atproto-oauth**: OAuth workflow implementation with storage
52
- **atproto-oauth-axum**: Axum web framework integration for OAuth
53
- **atproto-xrpcs**: XRPC service framework
54
- **atproto-xrpcs-helloworld**: Complete example XRPC service
···
59
- **Comprehensive error handling** with structured error types
60
- **Full test coverage** with unit tests across all modules
61
- **Modern dependencies** for HTTP, DNS, JSON, cryptographic operations, and WebSocket streaming
62
63
## Error Handling
64
···
138
- **`src/validation.rs`**: Input validation for handles and DIDs
139
- **`src/config.rs`**: Configuration management and environment variable handling
140
- **`src/errors.rs`**: Structured error types following project conventions
141
-
- **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256 and K-256 curves
142
143
### CLI Tools (require --features clap)
144
145
#### Identity Management (atproto-identity)
146
- **`src/bin/atproto-identity-resolve.rs`**: Resolve AT Protocol handles and DIDs to canonical identifiers
147
-
- **`src/bin/atproto-identity-key.rs`**: Generate and manage cryptographic keys (P-256, K-256)
148
- **`src/bin/atproto-identity-sign.rs`**: Create cryptographic signatures of JSON data
149
- **`src/bin/atproto-identity-validate.rs`**: Validate cryptographic signatures
150
···
23
#### Identity Management
24
- **Resolve identities**: `cargo run --features clap --bin atproto-identity-resolve -- <handle_or_did>`
25
- **Resolve with DID document**: `cargo run --features clap --bin atproto-identity-resolve -- --did-document <handle_or_did>`
26
+
- **Generate keys**: `cargo run --features clap --bin atproto-identity-key -- generate p256` (supports p256, p384, k256)
27
- **Sign data**: `cargo run --features clap --bin atproto-identity-sign -- <did_key> <json_file>`
28
- **Validate signatures**: `cargo run --features clap --bin atproto-identity-validate -- <did_key> <json_file> <signature>`
29
···
44
## Architecture
45
46
A comprehensive Rust workspace with multiple crates:
47
+
- **atproto-identity**: Core identity management with 11 modules (resolve, plc, web, model, validation, config, errors, key, storage, storage_lru, axum)
48
- **atproto-record**: Record signature operations and validation
49
- **atproto-client**: HTTP client with OAuth and identity integration
50
- **atproto-jetstream**: WebSocket event streaming with compression
51
+
- **atproto-oauth**: OAuth workflow implementation with DPoP, PKCE, JWT, and storage abstractions
52
+
- **atproto-oauth-aip**: AT Protocol OAuth AIP (Identity Provider) implementation with PAR support
53
- **atproto-oauth-axum**: Axum web framework integration for OAuth
54
- **atproto-xrpcs**: XRPC service framework
55
- **atproto-xrpcs-helloworld**: Complete example XRPC service
···
60
- **Comprehensive error handling** with structured error types
61
- **Full test coverage** with unit tests across all modules
62
- **Modern dependencies** for HTTP, DNS, JSON, cryptographic operations, and WebSocket streaming
63
+
- **Storage abstractions** with LRU caching support (optional via `lru` feature)
64
+
- **Axum integration** for web framework support (optional via `axum` feature)
65
+
- **Secure memory handling** (optional via `zeroize` feature)
66
67
## Error Handling
68
···
142
- **`src/validation.rs`**: Input validation for handles and DIDs
143
- **`src/config.rs`**: Configuration management and environment variable handling
144
- **`src/errors.rs`**: Structured error types following project conventions
145
+
- **`src/key.rs`**: Cryptographic key operations including signature validation and key identification for P-256, P-384, and K-256 curves
146
+
- **`src/storage.rs`**: Storage abstraction interface for DID document caching
147
+
- **`src/storage_lru.rs`**: LRU-based storage implementation (requires `lru` feature)
148
+
- **`src/axum.rs`**: Axum web framework integration (requires `axum` feature)
149
150
### CLI Tools (require --features clap)
151
152
#### Identity Management (atproto-identity)
153
- **`src/bin/atproto-identity-resolve.rs`**: Resolve AT Protocol handles and DIDs to canonical identifiers
154
+
- **`src/bin/atproto-identity-key.rs`**: Generate and manage cryptographic keys (P-256, P-384, K-256)
155
- **`src/bin/atproto-identity-sign.rs`**: Create cryptographic signatures of JSON data
156
- **`src/bin/atproto-identity-validate.rs`**: Validate cryptographic signatures
157
+18
Cargo.lock
+18
Cargo.lock
···
153
"thiserror 2.0.12",
154
"tokio",
155
"tracing",
156
]
157
158
[[package]]
···
207
"tokio",
208
"tracing",
209
"ulid",
210
]
211
212
[[package]]
···
220
"serde",
221
"serde_json",
222
"thiserror 2.0.12",
223
]
224
225
[[package]]
···
249
"tokio",
250
"tracing",
251
"urlencoding",
252
]
253
254
[[package]]
···
3397
version = "1.8.1"
3398
source = "registry+https://github.com/rust-lang/crates.io-index"
3399
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
3400
3401
[[package]]
3402
name = "zerotrie"
···
153
"thiserror 2.0.12",
154
"tokio",
155
"tracing",
156
+
"zeroize",
157
]
158
159
[[package]]
···
208
"tokio",
209
"tracing",
210
"ulid",
211
+
"zeroize",
212
]
213
214
[[package]]
···
222
"serde",
223
"serde_json",
224
"thiserror 2.0.12",
225
+
"zeroize",
226
]
227
228
[[package]]
···
252
"tokio",
253
"tracing",
254
"urlencoding",
255
+
"zeroize",
256
]
257
258
[[package]]
···
3401
version = "1.8.1"
3402
source = "registry+https://github.com/rust-lang/crates.io-index"
3403
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
3404
+
dependencies = [
3405
+
"zeroize_derive",
3406
+
]
3407
+
3408
+
[[package]]
3409
+
name = "zeroize_derive"
3410
+
version = "1.4.2"
3411
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3412
+
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
3413
+
dependencies = [
3414
+
"proc-macro2",
3415
+
"quote",
3416
+
"syn",
3417
+
]
3418
3419
[[package]]
3420
name = "zerotrie"
+2
Cargo.toml
+2
Cargo.toml
+1
-1
Dockerfile
+1
-1
Dockerfile
···
24
# - atproto-oauth-axum: 1 binary (oauth-tool)
25
# - atproto-xrpcs-helloworld: 1 binary (xrpcs-helloworld)
26
# - atproto-jetstream: 1 binary (jetstream-consumer)
27
-
RUN cargo build --release --bins -F clap
28
29
# Runtime stage - use distroless for minimal attack surface
30
FROM gcr.io/distroless/cc-debian12
···
24
# - atproto-oauth-axum: 1 binary (oauth-tool)
25
# - atproto-xrpcs-helloworld: 1 binary (xrpcs-helloworld)
26
# - atproto-jetstream: 1 binary (jetstream-consumer)
27
+
RUN cargo build --release --bins -F clap,zeroize
28
29
# Runtime stage - use distroless for minimal attack surface
30
FROM gcr.io/distroless/cc-debian12
+3
crates/atproto-identity/Cargo.toml
+3
crates/atproto-identity/Cargo.toml
···
66
axum = { version = "0.8", optional = true, features = ["macros"] }
67
http = { version = "1.0.0", optional = true }
68
69
+
zeroize = { workspace = true, optional = true }
70
+
71
[features]
72
default = ["lru", "axum"]
73
lru = ["dep:lru"]
74
axum = ["dep:axum", "dep:http"]
75
clap = ["dep:clap"]
76
+
zeroize = ["dep:zeroize"]
77
78
[lints]
79
workspace = true
+7
-2
crates/atproto-identity/src/key.rs
+7
-2
crates/atproto-identity/src/key.rs
···
60
61
use crate::errors::KeyError;
62
63
/// Cryptographic key types supported for AT Protocol identity.
64
#[cfg_attr(debug_assertions, derive(Debug))]
65
-
#[derive(Clone, PartialEq)]
66
pub enum KeyType {
67
/// A p256 (P-256 / secp256r1 / ES256) public key.
68
/// The multibase / multicodec prefix is 8024.
···
114
/// * `private_dpop_key_data`
115
///
116
#[derive(Clone)]
117
pub struct KeyData(pub KeyType, pub Vec<u8>);
118
119
impl KeyData {
···
134
135
/// Consumes self and returns the key type and bytes as a tuple.
136
pub fn into_parts(self) -> (KeyType, Vec<u8>) {
137
-
(self.0, self.1)
138
}
139
}
140
···
60
61
use crate::errors::KeyError;
62
63
+
#[cfg(feature = "zeroize")]
64
+
use zeroize::{Zeroize, ZeroizeOnDrop};
65
+
66
/// Cryptographic key types supported for AT Protocol identity.
67
+
#[derive(Clone, PartialEq)]
68
#[cfg_attr(debug_assertions, derive(Debug))]
69
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
70
pub enum KeyType {
71
/// A p256 (P-256 / secp256r1 / ES256) public key.
72
/// The multibase / multicodec prefix is 8024.
···
118
/// * `private_dpop_key_data`
119
///
120
#[derive(Clone)]
121
+
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
122
pub struct KeyData(pub KeyType, pub Vec<u8>);
123
124
impl KeyData {
···
139
140
/// Consumes self and returns the key type and bytes as a tuple.
141
pub fn into_parts(self) -> (KeyType, Vec<u8>) {
142
+
(self.0.clone(), self.1.clone())
143
}
144
}
145
+5
crates/atproto-oauth-aip/Cargo.toml
+5
crates/atproto-oauth-aip/Cargo.toml
+28
-4
crates/atproto-oauth-aip/src/workflow.rs
+28
-4
crates/atproto-oauth-aip/src/workflow.rs
···
117
//! for each phase of the OAuth flow including network failures, parsing errors,
118
//! and protocol violations.
119
120
-
use crate::errors::OAuthWorkflowError;
121
use anyhow::Result;
122
use atproto_oauth::{
123
resources::{AuthorizationServer, OAuthProtectedResource},
···
125
};
126
use serde::Deserialize;
127
128
/// OAuth client configuration containing essential client credentials.
129
pub struct OAuthClient {
130
/// The redirect URI where the authorization server will send the user after authorization.
131
pub redirect_uri: String,
132
/// The unique client identifier for this OAuth client.
133
pub client_id: String,
134
135
/// The client secret used for authenticating with the authorization server.
···
151
/// This structure contains all the information needed to make authenticated
152
/// requests to AT Protocol services after a successful OAuth flow.
153
#[derive(Clone, Deserialize)]
154
pub struct ATProtocolSession {
155
/// The Decentralized Identifier (DID) of the authenticated user.
156
pub did: String,
157
/// The handle (username) of the authenticated user.
158
pub handle: String,
159
/// The OAuth access token for making authenticated requests.
160
pub access_token: String,
161
/// The type of token (typically "Bearer").
162
pub token_type: String,
163
/// The list of OAuth scopes granted to this session.
164
pub scopes: Vec<String>,
165
/// The Personal Data Server (PDS) endpoint URL for this user.
166
pub pds_endpoint: String,
167
/// The DPoP (Demonstration of Proof-of-Possession) key in JWK format.
168
pub dpop_key: String,
169
/// Unix timestamp indicating when this session expires.
170
pub expires_at: i64,
171
}
172
173
#[derive(Deserialize, Clone)]
174
#[serde(untagged)]
175
enum WrappedATProtocolSession {
176
ATProtocolSession(ATProtocolSession),
177
Error {
178
error: String,
179
error_description: Option<String>,
···
411
.map_err(OAuthWorkflowError::SessionResponseParseFailed)?;
412
413
match response {
414
-
WrappedATProtocolSession::ATProtocolSession(value) => Ok(value),
415
WrappedATProtocolSession::Error {
416
-
error,
417
-
error_description,
418
} => {
419
let error_message = if let Some(value) = error_description {
420
format!("{error}: {value}")
···
117
//! for each phase of the OAuth flow including network failures, parsing errors,
118
//! and protocol violations.
119
120
use anyhow::Result;
121
use atproto_oauth::{
122
resources::{AuthorizationServer, OAuthProtectedResource},
···
124
};
125
use serde::Deserialize;
126
127
+
use crate::errors::OAuthWorkflowError;
128
+
129
+
#[cfg(feature = "zeroize")]
130
+
use zeroize::{Zeroize, ZeroizeOnDrop};
131
+
132
/// OAuth client configuration containing essential client credentials.
133
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
134
pub struct OAuthClient {
135
/// The redirect URI where the authorization server will send the user after authorization.
136
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
137
pub redirect_uri: String,
138
+
139
/// The unique client identifier for this OAuth client.
140
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
141
pub client_id: String,
142
143
/// The client secret used for authenticating with the authorization server.
···
159
/// This structure contains all the information needed to make authenticated
160
/// requests to AT Protocol services after a successful OAuth flow.
161
#[derive(Clone, Deserialize)]
162
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
163
pub struct ATProtocolSession {
164
/// The Decentralized Identifier (DID) of the authenticated user.
165
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
166
pub did: String,
167
+
168
/// The handle (username) of the authenticated user.
169
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
170
pub handle: String,
171
+
172
/// The OAuth access token for making authenticated requests.
173
pub access_token: String,
174
+
175
/// The type of token (typically "Bearer").
176
pub token_type: String,
177
+
178
/// The list of OAuth scopes granted to this session.
179
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
180
pub scopes: Vec<String>,
181
+
182
/// The Personal Data Server (PDS) endpoint URL for this user.
183
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
184
pub pds_endpoint: String,
185
+
186
/// The DPoP (Demonstration of Proof-of-Possession) key in JWK format.
187
pub dpop_key: String,
188
+
189
/// Unix timestamp indicating when this session expires.
190
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
191
pub expires_at: i64,
192
}
193
194
#[derive(Deserialize, Clone)]
195
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
196
#[serde(untagged)]
197
enum WrappedATProtocolSession {
198
ATProtocolSession(ATProtocolSession),
199
+
200
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
201
Error {
202
error: String,
203
error_description: Option<String>,
···
435
.map_err(OAuthWorkflowError::SessionResponseParseFailed)?;
436
437
match response {
438
+
WrappedATProtocolSession::ATProtocolSession(ref value) => Ok(value.clone()),
439
WrappedATProtocolSession::Error {
440
+
ref error,
441
+
ref error_description,
442
} => {
443
let error_message = if let Some(value) = error_description {
444
format!("{error}: {value}")
+3
crates/atproto-oauth-axum/Cargo.toml
+3
crates/atproto-oauth-axum/Cargo.toml
···
47
rpassword = { workspace = true, optional = true }
48
secrecy = { workspace = true, optional = true }
49
50
+
zeroize = { workspace = true, optional = true }
51
+
52
[features]
53
clap = ["dep:clap", "dep:rpassword", "dep:secrecy"]
54
+
zeroize = ["dep:zeroize", "atproto-identity/zeroize", "atproto-oauth/zeroize"]
55
56
[lints]
57
workspace = true
+1
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
+1
crates/atproto-oauth-axum/src/bin/atproto-oauth-tool.rs
+3
-3
crates/atproto-oauth-axum/src/handle_complete.rs
+3
-3
crates/atproto-oauth-axum/src/handle_complete.rs
···
92
let private_dpop_key_data = identify_key(&oauth_request.dpop_private_key)?;
93
94
let oauth_client = OAuthClient {
95
-
redirect_uri: oauth_client_config.redirect_uris,
96
-
client_id: oauth_client_config.client_id,
97
private_signing_key_data,
98
};
99
···
127
token_response.token_type,
128
token_response.expires_in,
129
token_response.scope,
130
-
token_response.sub.unwrap_or("unknown".to_string()),
131
private_dpop_key_data
132
);
133
···
92
let private_dpop_key_data = identify_key(&oauth_request.dpop_private_key)?;
93
94
let oauth_client = OAuthClient {
95
+
redirect_uri: oauth_client_config.redirect_uris.clone(),
96
+
client_id: oauth_client_config.client_id.clone(),
97
private_signing_key_data,
98
};
99
···
127
token_response.token_type,
128
token_response.expires_in,
129
token_response.scope,
130
+
token_response.sub.clone().unwrap_or("unknown".to_string()),
131
private_dpop_key_data
132
);
133
-4
crates/atproto-oauth-axum/src/handle_init.rs
-4
crates/atproto-oauth-axum/src/handle_init.rs
-1
crates/atproto-oauth-axum/src/lib.rs
-1
crates/atproto-oauth-axum/src/lib.rs
+21
crates/atproto-oauth-axum/src/state.rs
+21
crates/atproto-oauth-axum/src/state.rs
···
8
use http::request::Parts;
9
use std::convert::Infallible;
10
11
/// OAuth client configuration for Axum handlers.
12
///
13
/// Contains the essential configuration needed for OAuth client operations.
14
#[derive(Clone, Default)]
15
pub struct OAuthClientConfig {
16
/// OAuth client identifier
17
pub client_id: String,
18
/// Allowed OAuth redirect URIs
19
pub redirect_uris: String,
20
/// JSON Web Key Set URI for public keys
21
pub jwks_uri: Option<String>,
22
/// Signing keys for JWT operations
23
pub signing_keys: Vec<KeyData>,
24
/// OAuth scope, defaults to "atproto transition:generic"
25
pub scope: Option<String>,
26
27
/// Optional human-readable client name
28
pub client_name: Option<String>,
29
/// Optional client website URI
30
pub client_uri: Option<String>,
31
/// Optional client logo URI
32
pub logo_uri: Option<String>,
33
/// Optional terms of service URI
34
pub tos_uri: Option<String>,
35
/// Optional privacy policy URI
36
pub policy_uri: Option<String>,
37
}
38
···
8
use http::request::Parts;
9
use std::convert::Infallible;
10
11
+
#[cfg(feature = "zeroize")]
12
+
use zeroize::{Zeroize, ZeroizeOnDrop};
13
+
14
/// OAuth client configuration for Axum handlers.
15
///
16
/// Contains the essential configuration needed for OAuth client operations.
17
#[derive(Clone, Default)]
18
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
19
pub struct OAuthClientConfig {
20
/// OAuth client identifier
21
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
22
pub client_id: String,
23
+
24
/// Allowed OAuth redirect URIs
25
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
26
pub redirect_uris: String,
27
+
28
/// JSON Web Key Set URI for public keys
29
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
30
pub jwks_uri: Option<String>,
31
+
32
/// Signing keys for JWT operations
33
pub signing_keys: Vec<KeyData>,
34
+
35
/// OAuth scope, defaults to "atproto transition:generic"
36
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
37
pub scope: Option<String>,
38
39
/// Optional human-readable client name
40
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
41
pub client_name: Option<String>,
42
+
43
/// Optional client website URI
44
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
45
pub client_uri: Option<String>,
46
+
47
/// Optional client logo URI
48
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
49
pub logo_uri: Option<String>,
50
+
51
/// Optional terms of service URI
52
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
53
pub tos_uri: Option<String>,
54
+
55
/// Optional privacy policy URI
56
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
57
pub policy_uri: Option<String>,
58
}
59
+3
crates/atproto-oauth/Cargo.toml
+3
crates/atproto-oauth/Cargo.toml
···
44
axum = { version = "0.8", optional = true }
45
http = { version = "1.0.0", optional = true }
46
47
+
zeroize = { workspace = true, optional = true }
48
+
49
[features]
50
default = ["lru", "axum"]
51
lru = ["dep:lru"]
52
axum = ["dep:axum", "dep:http"]
53
+
zeroize = ["dep:zeroize", "atproto-identity/zeroize"]
54
55
[lints]
56
workspace = true
+2
-2
crates/atproto-oauth/src/dpop.rs
+2
-2
crates/atproto-oauth/src/dpop.rs
···
342
type_: Some("dpop+jwt".to_string()),
343
algorithm: Some("ES256".to_string()),
344
json_web_key: Some(dpop_jwk),
345
-
..Default::default()
346
};
347
348
let auth = access_token.map(challenge);
···
1252
type_: Some("dpop+jwt".to_string()),
1253
algorithm: Some("ES256".to_string()),
1254
json_web_key: Some(dpop_jwk),
1255
-
..Default::default()
1256
};
1257
1258
let claims = Claims::new(JoseClaims {
···
342
type_: Some("dpop+jwt".to_string()),
343
algorithm: Some("ES256".to_string()),
344
json_web_key: Some(dpop_jwk),
345
+
key_id: None,
346
};
347
348
let auth = access_token.map(challenge);
···
1252
type_: Some("dpop+jwt".to_string()),
1253
algorithm: Some("ES256".to_string()),
1254
json_web_key: Some(dpop_jwk),
1255
+
key_id: None,
1256
};
1257
1258
let claims = Claims::new(JoseClaims {
+8
-1
crates/atproto-oauth/src/jwk.rs
+8
-1
crates/atproto-oauth/src/jwk.rs
···
15
16
use crate::errors::JWKError;
17
18
/// A wrapped JSON Web Key with additional metadata.
19
#[cfg_attr(debug_assertions, derive(Debug))]
20
-
#[derive(Serialize, Deserialize, Clone, PartialEq)]
21
pub struct WrappedJsonWebKey {
22
/// Key identifier (kid) for the JWK.
23
#[serde(skip_serializing_if = "Option::is_none", default)]
24
pub kid: Option<String>,
25
26
/// Algorithm (alg) used with this key.
27
#[serde(skip_serializing_if = "Option::is_none", default)]
28
pub alg: Option<String>,
29
30
/// Public key use (use) parameter, typically "sig" for signature operations.
31
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
32
pub _use: Option<String>,
33
34
/// The underlying elliptic curve JWK.
···
15
16
use crate::errors::JWKError;
17
18
+
#[cfg(feature = "zeroize")]
19
+
use zeroize::{Zeroize, ZeroizeOnDrop};
20
+
21
/// A wrapped JSON Web Key with additional metadata.
22
+
#[derive(Serialize, Deserialize, Clone, PartialEq)]
23
#[cfg_attr(debug_assertions, derive(Debug))]
24
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
25
pub struct WrappedJsonWebKey {
26
/// Key identifier (kid) for the JWK.
27
#[serde(skip_serializing_if = "Option::is_none", default)]
28
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
29
pub kid: Option<String>,
30
31
/// Algorithm (alg) used with this key.
32
#[serde(skip_serializing_if = "Option::is_none", default)]
33
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
34
pub alg: Option<String>,
35
36
/// Public key use (use) parameter, typically "sig" for signature operations.
37
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
38
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
39
pub _use: Option<String>,
40
41
/// The underlying elliptic curve JWK.
+12
-4
crates/atproto-oauth/src/jwt.rs
+12
-4
crates/atproto-oauth/src/jwt.rs
···
4
//! custom extensions for AT Protocol OAuth flows. Supports ES256, ES384, and ES256K elliptic curve
5
//! signature algorithms with comprehensive timestamp validation and structured error handling.
6
7
-
use crate::encoding::ToBase64;
8
-
use crate::errors::JWTError;
9
use anyhow::Result;
10
use atproto_identity::key::{KeyData, KeyType, sign, to_public, validate};
11
use base64::{Engine as _, engine::general_purpose};
···
14
use std::collections::BTreeMap;
15
use std::time::{SystemTime, UNIX_EPOCH};
16
17
/// JWT header containing algorithm and key metadata.
18
#[cfg_attr(debug_assertions, derive(Debug))]
19
-
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
20
pub struct Header {
21
/// Algorithm used for signing (e.g., "ES256", "ES384", "ES256K").
22
#[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
···
175
(Some("ES384"), KeyType::P384Private) | (Some("ES384"), KeyType::P384Public) => {}
176
_ => {
177
return Err(JWTError::UnsupportedAlgorithm {
178
-
algorithm: header.algorithm.unwrap_or_else(|| "none".to_string()),
179
key_type: format!("{}", key_data.key_type()),
180
}
181
.into());
···
4
//! custom extensions for AT Protocol OAuth flows. Supports ES256, ES384, and ES256K elliptic curve
5
//! signature algorithms with comprehensive timestamp validation and structured error handling.
6
7
use anyhow::Result;
8
use atproto_identity::key::{KeyData, KeyType, sign, to_public, validate};
9
use base64::{Engine as _, engine::general_purpose};
···
12
use std::collections::BTreeMap;
13
use std::time::{SystemTime, UNIX_EPOCH};
14
15
+
use crate::encoding::ToBase64;
16
+
use crate::errors::JWTError;
17
+
18
+
#[cfg(feature = "zeroize")]
19
+
use zeroize::{Zeroize, ZeroizeOnDrop};
20
+
21
/// JWT header containing algorithm and key metadata.
22
+
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
23
#[cfg_attr(debug_assertions, derive(Debug))]
24
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
25
pub struct Header {
26
/// Algorithm used for signing (e.g., "ES256", "ES384", "ES256K").
27
#[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
···
180
(Some("ES384"), KeyType::P384Private) | (Some("ES384"), KeyType::P384Public) => {}
181
_ => {
182
return Err(JWTError::UnsupportedAlgorithm {
183
+
algorithm: header
184
+
.algorithm
185
+
.clone()
186
+
.unwrap_or_else(|| "none".to_string()),
187
key_type: format!("{}", key_data.key_type()),
188
}
189
.into());
+42
-8
crates/atproto-oauth/src/workflow.rs
+42
-8
crates/atproto-oauth/src/workflow.rs
···
76
//! ).await?;
77
//! ```
78
79
-
use crate::{
80
-
dpop::{DpopRetry, auth_dpop},
81
-
errors::OAuthClientError,
82
-
jwt::{Claims, Header, JoseClaims, mint},
83
-
resources::{AuthorizationServer, pds_resources},
84
-
};
85
use atproto_identity::key::KeyData;
86
use chrono::{DateTime, Utc};
87
use rand::distributions::{Alphanumeric, DistString};
88
use reqwest_chain::ChainMiddleware;
89
use reqwest_middleware::ClientBuilder;
90
-
91
use std::collections::HashMap;
92
93
-
use serde::Deserialize;
94
95
/// Response from a Pushed Authorization Request (PAR) endpoint.
96
///
···
127
///
128
/// This struct holds the client configuration needed for OAuth authorization flows,
129
/// including the redirect URI, client identifier, and signing key.
130
pub struct OAuthClient {
131
/// The redirect URI where the authorization server will send the user after authorization.
132
pub redirect_uri: String,
133
/// The unique client identifier for this OAuth client.
134
pub client_id: String,
135
/// The private key data used for signing client assertions.
136
pub private_signing_key_data: KeyData,
137
}
···
141
/// This struct contains all the necessary information to track and complete
142
/// an OAuth authorization request, including security parameters and timing.
143
#[derive(Clone, PartialEq)]
144
pub struct OAuthRequest {
145
/// The OAuth state parameter used to prevent CSRF attacks.
146
pub oauth_state: String,
147
/// The authorization server issuer identifier.
148
pub issuer: String,
149
/// The DID (Decentralized Identifier) of the user.
150
pub did: String,
151
/// The nonce value for additional security.
152
pub nonce: String,
153
/// The PKCE code verifier for this authorization request.
154
pub pkce_verifier: String,
155
/// The public key used for signing (serialized).
156
pub signing_public_key: String,
157
/// The DPoP private key (serialized).
158
pub dpop_private_key: String,
159
/// When this OAuth request was created.
160
pub created_at: DateTime<Utc>,
161
/// When this OAuth request expires.
162
pub expires_at: DateTime<Utc>,
163
}
164
···
183
/// This struct represents the successful response from an OAuth token exchange,
184
/// containing the access token and related metadata.
185
#[derive(Clone, Deserialize)]
186
pub struct TokenResponse {
187
/// The access token that can be used to access protected resources.
188
pub access_token: String,
189
/// The type of token, typically "Bearer" or "DPoP".
190
pub token_type: String,
191
/// The refresh token that can be used to obtain new access tokens.
192
pub refresh_token: String,
193
/// The scope of access granted by the access token.
194
pub scope: String,
195
/// The lifetime of the access token in seconds.
196
pub expires_in: u32,
197
/// The subject identifier (usually the user's DID).
198
pub sub: Option<String>,
199
200
/// Additional fields returned by the authorization server.
201
#[serde(flatten)]
202
pub extra: HashMap<String, serde_json::Value>,
203
}
204
···
76
//! ).await?;
77
//! ```
78
79
use atproto_identity::key::KeyData;
80
use chrono::{DateTime, Utc};
81
use rand::distributions::{Alphanumeric, DistString};
82
use reqwest_chain::ChainMiddleware;
83
use reqwest_middleware::ClientBuilder;
84
+
use serde::Deserialize;
85
use std::collections::HashMap;
86
87
+
#[cfg(feature = "zeroize")]
88
+
use zeroize::{Zeroize, ZeroizeOnDrop};
89
+
90
+
use crate::{
91
+
dpop::{DpopRetry, auth_dpop},
92
+
errors::OAuthClientError,
93
+
jwt::{Claims, Header, JoseClaims, mint},
94
+
resources::{AuthorizationServer, pds_resources},
95
+
};
96
97
/// Response from a Pushed Authorization Request (PAR) endpoint.
98
///
···
129
///
130
/// This struct holds the client configuration needed for OAuth authorization flows,
131
/// including the redirect URI, client identifier, and signing key.
132
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
133
pub struct OAuthClient {
134
/// The redirect URI where the authorization server will send the user after authorization.
135
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
136
pub redirect_uri: String,
137
+
138
/// The unique client identifier for this OAuth client.
139
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
140
pub client_id: String,
141
+
142
/// The private key data used for signing client assertions.
143
pub private_signing_key_data: KeyData,
144
}
···
148
/// This struct contains all the necessary information to track and complete
149
/// an OAuth authorization request, including security parameters and timing.
150
#[derive(Clone, PartialEq)]
151
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
152
pub struct OAuthRequest {
153
/// The OAuth state parameter used to prevent CSRF attacks.
154
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
155
pub oauth_state: String,
156
+
157
/// The authorization server issuer identifier.
158
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
159
pub issuer: String,
160
+
161
/// The DID (Decentralized Identifier) of the user.
162
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
163
pub did: String,
164
+
165
/// The nonce value for additional security.
166
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
167
pub nonce: String,
168
+
169
/// The PKCE code verifier for this authorization request.
170
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
171
pub pkce_verifier: String,
172
+
173
/// The public key used for signing (serialized).
174
pub signing_public_key: String,
175
+
176
/// The DPoP private key (serialized).
177
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
178
pub dpop_private_key: String,
179
+
180
/// When this OAuth request was created.
181
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
182
pub created_at: DateTime<Utc>,
183
+
184
/// When this OAuth request expires.
185
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
186
pub expires_at: DateTime<Utc>,
187
}
188
···
207
/// This struct represents the successful response from an OAuth token exchange,
208
/// containing the access token and related metadata.
209
#[derive(Clone, Deserialize)]
210
+
#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
211
pub struct TokenResponse {
212
/// The access token that can be used to access protected resources.
213
pub access_token: String,
214
+
215
/// The type of token, typically "Bearer" or "DPoP".
216
pub token_type: String,
217
+
218
/// The refresh token that can be used to obtain new access tokens.
219
pub refresh_token: String,
220
+
221
/// The scope of access granted by the access token.
222
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
223
pub scope: String,
224
+
225
/// The lifetime of the access token in seconds.
226
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
227
pub expires_in: u32,
228
+
229
/// The subject identifier (usually the user's DID).
230
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
231
pub sub: Option<String>,
232
233
/// Additional fields returned by the authorization server.
234
#[serde(flatten)]
235
+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
236
pub extra: HashMap<String, serde_json::Value>,
237
}
238