diff --git a/Cargo.lock b/Cargo.lock index 9858340..6e953ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,9 +191,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atproto-client" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e26a266e571fd9cb401f47a2183ee2a1376e5dbd28b453aa3f5d2b6aa8e2f9b" +checksum = "b8b3d9628afd1e4a79e9701089aced14c2519c12f4f8b27f4a6e72bb3eb29e66" dependencies = [ "anyhow", "atproto-identity", @@ -213,17 +213,15 @@ dependencies = [ [[package]] name = "atproto-identity" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eafb629927bd1e4f336edfd6a86e525d72851b7efbf173eb5f1e35a4395e95" +checksum = "0091aac30b30c70507c865d3b09053178a746f474742eba7f25377086d13bcba" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", "ecdsa", "elliptic-curve", "hickory-resolver", - "http 1.3.1", "k256", "lru", "multibase", @@ -242,19 +240,17 @@ dependencies = [ [[package]] name = "atproto-oauth" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4d911a2eb8bf626fc7c2857fc3d31e1392278e53fc05642824894844a7f37f" +checksum = "58743096f464928c76b5e8ed73b4218345c66723629dc623cb93662cc8ecd4e6" dependencies = [ "anyhow", "async-trait", "atproto-identity", - "axum 0.8.4", "base64", "chrono", "ecdsa", "elliptic-curve", - "http 1.3.1", "k256", "lru", "multibase", @@ -277,9 +273,9 @@ dependencies = [ [[package]] name = "atproto-oauth-axum" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8319c89de868b647fdf5e2b077e81bd0dcff54c49dd1b9798c5d296ba102aa" +checksum = "ea053c73ad9c19ea63a355193833728a61156744d129e72a62d4ffe361133bb1" dependencies = [ "anyhow", "async-trait", @@ -300,36 +296,30 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "urlencoding", "zeroize", ] [[package]] name = "atproto-record" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640a09da90a3460a60ff85e26ab70735a3ec20ea3b3d28a37278d93ebe0901dd" +checksum = "66191a5512ebfa103e72ddbc3b96d4d5afd58e22dbcadf4cc7d5ea431c1cb31e" dependencies = [ "anyhow", "atproto-identity", + "base64", "chrono", - "ecdsa", - "k256", - "multibase", - "p256", "serde", "serde_ipld_dagcbor", "serde_json", "thiserror 2.0.12", - "tokio", - "tracing", ] [[package]] name = "atproto-xrpcs" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a5127ae4366cf58a717a2e2ec263f5650b33ea0f63968b04d5f7d5b22a1399" +checksum = "12c18b713888b1af7027770db1815d7508ed4bd666a4bf5e96515915d8ea3654" dependencies = [ "anyhow", "async-trait", @@ -351,7 +341,6 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "urlencoding", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 182181c..0fcc624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "aip" version = "0.1.0" edition = "2024" -rust-version = "1.87" +rust-version = "1.89" [[bin]] name = "aip" @@ -18,11 +18,12 @@ path = "src/lib.rs" [dependencies] axum = { version = "0.8" } -atproto-client = { version = "0.9.6" } -atproto-identity = { version = "0.9.6", features = ["zeroize"] } -atproto-oauth = { version = "0.9.6", features = ["zeroize"] } -atproto-oauth-axum = { version = "0.9.6", features = ["zeroize"] } -atproto-xrpcs = { version = "0.9.6" } + +atproto-identity = { version = "0.11.0", features = ["lru", "zeroize", "hickory-dns"] } +atproto-oauth = { version = "0.11.0", features = ["lru", "zeroize", "hickory-dns"] } +atproto-oauth-axum = { version = "0.11.0", features = ["zeroize"] } +atproto-client = { version = "0.11.0" } +atproto-xrpcs = { version = "0.11.0", features = ["hickory-dns"] } axum-template = { version = "3.0", features = ["minijinja"] } minijinja = { version = "2.7", features = ["builtins"] } diff --git a/Dockerfile b/Dockerfile index 296f6a5..fbe4acb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM rust:1.87-slim AS builder +FROM rust:1.89-slim AS builder # Install required system dependencies for building RUN apt-get update && apt-get install -y \ diff --git a/src/bin/aip.rs b/src/bin/aip.rs index d8d0c60..43f5000 100644 --- a/src/bin/aip.rs +++ b/src/bin/aip.rs @@ -20,7 +20,7 @@ use aip::{ }; use anyhow::Result; use atproto_identity::{ - resolve::{IdentityResolver, InnerIdentityResolver, create_resolver}, + resolve::{HickoryDnsResolver, InnerIdentityResolver, SharedIdentityResolver}, storage::DidDocumentStorage, storage_lru::LruDidDocumentStorage, }; @@ -122,10 +122,12 @@ async fn main() -> Result<()> { }; // Initialize the DNS resolver - let dns_resolver = create_resolver(config.dns_nameservers.as_ref()); + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver( + config.dns_nameservers.as_ref(), + )); // Initialize the identity resolver - let identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver { + let identity_resolver = SharedIdentityResolver(Arc::new(InnerIdentityResolver { dns_resolver, http_client: http_client.clone(), plc_hostname: config.plc_hostname.clone(), diff --git a/src/config.rs b/src/config.rs index 6e9430d..7375d1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ //! Environment-based configuration types for AIP server runtime settings. use anyhow::Result; -use atproto_identity::key::{KeyData, KeyType, generate_key, identify_key}; +use atproto_identity::key::{KeyData, identify_key}; use std::time::Duration; use crate::errors::ConfigError; @@ -329,13 +329,15 @@ impl TryFrom> for PrivateKeys { match value { None => { // Generate a new P-256 private key if no keys are provided - let key = generate_key(KeyType::P256Private)?; - Ok(Self(vec![key])) + // let key = generate_key(KeyType::P256Private)?; + // Ok(Self(vec![key])) + unreachable!() } Some(value) if value.is_empty() => { // Generate a new P-256 private key if no keys are provided - let key = generate_key(KeyType::P256Private)?; - Ok(Self(vec![key])) + // let key = generate_key(KeyType::P256Private)?; + // Ok(Self(vec![key])) + unreachable!() } Some(value) => { // Parse semicolon-separated list of KeyData DID strings @@ -347,8 +349,9 @@ impl TryFrom> for PrivateKeys { if keys.is_empty() { // Generate a new P-256 private key if parsing resulted in empty list - let key = generate_key(KeyType::P256Private)?; - Ok(Self(vec![key])) + // let key = generate_key(KeyType::P256Private)?; + // Ok(Self(vec![key])) + unreachable!() } else { Ok(Self(keys)) } diff --git a/src/http/context.rs b/src/http/context.rs index d6f2095..f8f1721 100644 --- a/src/http/context.rs +++ b/src/http/context.rs @@ -1,9 +1,8 @@ //! Application state and request context management. use atproto_identity::{ - axum::state::DidDocumentStorageExtractor, key::{KeyData, KeyProvider}, - resolve::IdentityResolver, + resolve::SharedIdentityResolver, storage::DidDocumentStorage, }; use atproto_oauth::storage::OAuthRequestStorage; @@ -41,7 +40,7 @@ pub struct AppState { /// Template engine for rendering HTML responses. pub template_env: AppEngine, /// Identity resolver for ATProtocol DIDs - pub identity_resolver: IdentityResolver, + pub identity_resolver: SharedIdentityResolver, /// Key provider for OAuth signing keys pub key_provider: Arc, /// OAuth request storage for ATProtocol flows @@ -69,10 +68,8 @@ impl FromRef for Arc { } } -impl FromRef for DidDocumentStorageExtractor { +impl FromRef for Arc { fn from_ref(app_state: &AppState) -> Self { - atproto_identity::axum::state::DidDocumentStorageExtractor( - app_state.document_storage.clone(), - ) + app_state.document_storage.clone() } } diff --git a/src/http/handler_index.rs b/src/http/handler_index.rs index 7cd5b71..dc5599e 100644 --- a/src/http/handler_index.rs +++ b/src/http/handler_index.rs @@ -24,7 +24,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -33,8 +33,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/http/handler_oauth.rs b/src/http/handler_oauth.rs index 395ed6b..f382ecf 100644 --- a/src/http/handler_oauth.rs +++ b/src/http/handler_oauth.rs @@ -117,7 +117,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -126,8 +126,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/http/handler_par.rs b/src/http/handler_par.rs index 2d62940..067f884 100644 --- a/src/http/handler_par.rs +++ b/src/http/handler_par.rs @@ -38,7 +38,7 @@ pub(super) struct PushedAuthorizationRequest { // ATProtocol-specific parameter (legacy, prefer login_hint) pub subject: Option, - + /// JWT client assertion for private_key_jwt authentication (RFC 7523) pub client_assertion: Option, /// Client assertion type for private_key_jwt authentication @@ -284,11 +284,13 @@ fn extract_client_auth_from_headers(headers: &HeaderMap) -> Option Option { +fn extract_client_auth_from_request( + request: &PushedAuthorizationRequest, +) -> Option { // Check for JWT client assertion first (private_key_jwt) - if let (Some(client_assertion), Some(client_assertion_type)) = - (&request.client_assertion, &request.client_assertion_type) { - + if let (Some(client_assertion), Some(client_assertion_type)) = + (&request.client_assertion, &request.client_assertion_type) + { // Validate the assertion type if client_assertion_type == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { return Some(ClientAuthentication { @@ -299,7 +301,7 @@ fn extract_client_auth_from_request(request: &PushedAuthorizationRequest) -> Opt }); } } - + // PAR typically uses client credentials from headers, not form data // But we'll support client_id from the form Some(ClientAuthentication { @@ -339,20 +341,25 @@ fn authenticate_client( ClientAuthMethod::PrivateKeyJwt => { // Require JWT client assertion if let Some(client_assertion) = client_auth.client_assertion.as_ref() { - // Construct token endpoint URL for audience validation + // Construct token endpoint URL for audience validation // Note: PAR uses token endpoint as audience per RFC 9126 let token_endpoint = format!("{}/oauth/token", issuer); - + // Validate the JWT client assertion let par_endpoint = format!("{}/oauth/par", issuer); - match validate_client_assertion(client_assertion, client, &token_endpoint, Some(&par_endpoint)) { + match validate_client_assertion( + client_assertion, + client, + &token_endpoint, + Some(&par_endpoint), + ) { Ok(validated_client_id) => { // Ensure the validated client_id matches the expected client if validated_client_id == client.client_id { Ok(()) } else { Err(OAuthError::InvalidClient( - "JWT client_id does not match expected client".to_string() + "JWT client_id does not match expected client".to_string(), )) } } @@ -360,7 +367,7 @@ fn authenticate_client( } } else { Err(OAuthError::InvalidClient( - "Missing client_assertion for private_key_jwt authentication".to_string() + "Missing client_assertion for private_key_jwt authentication".to_string(), )) } } diff --git a/src/http/handler_userinfo.rs b/src/http/handler_userinfo.rs index 52c795c..844ca72 100644 --- a/src/http/handler_userinfo.rs +++ b/src/http/handler_userinfo.rs @@ -118,7 +118,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -127,8 +127,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/http/handler_well_known.rs b/src/http/handler_well_known.rs index cd0ad86..0455651 100644 --- a/src/http/handler_well_known.rs +++ b/src/http/handler_well_known.rs @@ -130,8 +130,10 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = atproto_identity::resolve::create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new( + atproto_identity::resolve::HickoryDnsResolver::create_resolver(&dns_nameservers), + ); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/http/server.rs b/src/http/server.rs index f0fd267..e060b35 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -4,7 +4,11 @@ use axum::{ Router, middleware, routing::{get, post}, }; +use std::time::Duration; +use tower_http::classify::ServerErrorsFailureClass; +use tower_http::trace::DefaultMakeSpan; use tower_http::{cors::CorsLayer, services::ServeDir, trace::TraceLayer}; +use tracing::Span; use super::{ context::AppState, @@ -120,6 +124,19 @@ pub fn build_router(ctx: AppState) -> Router { post(xrpc_clients_update_handler), ) .nest_service("/static", ServeDir::new(&ctx.config.http_static_path)) + .layer( + TraceLayer::new_for_http() + .make_span_with( + DefaultMakeSpan::new() + .level(tracing::Level::INFO) + .include_headers(true), + ) + .on_failure( + |err: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { + tracing::error!(error = ?err, "Unhandled error: {err}"); + }, + ), + ) .layer(cors) .layer(TraceLayer::new_for_http()) .with_state(ctx) @@ -131,7 +148,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -146,8 +163,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/http/utils_oauth.rs b/src/http/utils_oauth.rs index 52388ff..d18cceb 100644 --- a/src/http/utils_oauth.rs +++ b/src/http/utils_oauth.rs @@ -119,7 +119,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -128,8 +128,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/oauth/atprotocol_bridge.rs b/src/oauth/atprotocol_bridge.rs index 1869be0..061ecb4 100644 --- a/src/oauth/atprotocol_bridge.rs +++ b/src/oauth/atprotocol_bridge.rs @@ -89,7 +89,7 @@ pub struct AtpBackedAuthorizationServer { /// Base OAuth authorization server base_auth_server: Arc, /// ATProtocol identity resolver - identity_resolver: atproto_identity::resolve::IdentityResolver, + identity_resolver: atproto_identity::resolve::SharedIdentityResolver, /// HTTP client for making requests http_client: reqwest::Client, /// OAuth request storage for ATProtocol flows @@ -110,7 +110,7 @@ impl AtpBackedAuthorizationServer { /// Create a new ATProtocol-backed authorization server pub fn new( base_auth_server: Arc, - identity_resolver: atproto_identity::resolve::IdentityResolver, + identity_resolver: atproto_identity::resolve::SharedIdentityResolver, http_client: reqwest::Client, oauth_request_storage: Arc, client_config: atproto_oauth_axum::state::OAuthClientConfig, @@ -868,7 +868,9 @@ mod tests { UnifiedAtpOAuthSessionStorageAdapter, UnifiedAuthorizationRequestStorageAdapter, }; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::resolve::{IdentityResolver, InnerIdentityResolver, create_resolver}; + use atproto_identity::resolve::{ + HickoryDnsResolver, InnerIdentityResolver, SharedIdentityResolver, + }; use atproto_identity::storage_lru::LruDidDocumentStorage; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::num::NonZeroUsize; @@ -887,8 +889,8 @@ mod tests { // Create identity resolver let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver { + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = SharedIdentityResolver(Arc::new(InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, plc_hostname: "plc.directory".to_string(), diff --git a/src/oauth/auth_server.rs b/src/oauth/auth_server.rs index 46ac910..1a2b953 100644 --- a/src/oauth/auth_server.rs +++ b/src/oauth/auth_server.rs @@ -3,8 +3,8 @@ use crate::errors::OAuthError; use crate::oauth::{dpop::*, types::*}; use crate::storage::traits::OAuthStorage; -use atproto_oauth::jwk::{WrappedJsonWebKey, to_key_data}; use atproto_identity::key::KeyType; +use atproto_oauth::jwk::{WrappedJsonWebKey, to_key_data}; use axum::{ Form, extract::{Query, State}, @@ -539,20 +539,21 @@ impl AuthorizationServer { // Require JWT client assertion if let Some(client_assertion) = client_auth .as_ref() - .and_then(|auth| auth.client_assertion.as_ref()) { - + .and_then(|auth| auth.client_assertion.as_ref()) + { // Construct token endpoint URL for audience validation let token_endpoint = format!("{}/oauth/token", self.issuer); - + // Validate the JWT client assertion - match validate_client_assertion(client_assertion, client, &token_endpoint, None) { + match validate_client_assertion(client_assertion, client, &token_endpoint, None) + { Ok(validated_client_id) => { // Ensure the validated client_id matches the expected client if validated_client_id == client.client_id { Ok(()) } else { Err(OAuthError::InvalidClient( - "JWT client_id does not match expected client".to_string() + "JWT client_id does not match expected client".to_string(), )) } } @@ -560,7 +561,7 @@ impl AuthorizationServer { } } else { Err(OAuthError::InvalidClient( - "Missing client_assertion for private_key_jwt authentication".to_string() + "Missing client_assertion for private_key_jwt authentication".to_string(), )) } } @@ -762,9 +763,9 @@ pub async fn token_handler( /// Extract client authentication from headers and form pub fn extract_client_auth(headers: &HeaderMap, form: &TokenForm) -> Option { // Check for JWT client assertion first (private_key_jwt) - if let (Some(client_assertion), Some(client_assertion_type)) = - (&form.client_assertion, &form.client_assertion_type) { - + if let (Some(client_assertion), Some(client_assertion_type)) = + (&form.client_assertion, &form.client_assertion_type) + { // Validate the assertion type if client_assertion_type == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" { // Extract client_id from form (client_id is required in form for private_key_jwt) @@ -816,7 +817,7 @@ pub fn extract_client_auth(headers: &HeaderMap, form: &TokenForm) -> Option, @@ -827,55 +828,67 @@ pub fn validate_client_assertion( return Err(OAuthError::InvalidClient("Invalid JWT format".to_string())); } - // Parse header to extract algorithm and key ID (if present) - let header_json = BASE64_URL_SAFE_NO_PAD.decode(parts[0]) + // Parse header to extract algorithm and key ID (if present) + let header_json = BASE64_URL_SAFE_NO_PAD + .decode(parts[0]) .map_err(|_| OAuthError::InvalidClient("Invalid JWT header".to_string()))?; let header: serde_json::Value = serde_json::from_slice(&header_json) .map_err(|_| OAuthError::InvalidClient("Invalid JWT header JSON".to_string()))?; // Extract algorithm from header - let alg = header.get("alg") + let alg = header + .get("alg") .and_then(|v| v.as_str()) .ok_or_else(|| OAuthError::InvalidClient("Missing 'alg' in JWT header".to_string()))?; - + // Extract key ID if present (kid is optional for private_key_jwt) let kid = header.get("kid").and_then(|v| v.as_str()); - + // Parse claims to extract subject, issuer, audience, expiration - let claims_json = BASE64_URL_SAFE_NO_PAD.decode(parts[1]) + let claims_json = BASE64_URL_SAFE_NO_PAD + .decode(parts[1]) .map_err(|_| OAuthError::InvalidClient("Invalid JWT claims".to_string()))?; let claims: serde_json::Value = serde_json::from_slice(&claims_json) .map_err(|_| OAuthError::InvalidClient("Invalid JWT claims JSON".to_string()))?; // Validate required claims per RFC 7523 - let sub = claims.get("sub") + let sub = claims + .get("sub") .and_then(|v| v.as_str()) .ok_or_else(|| OAuthError::InvalidClient("Missing 'sub' claim".to_string()))?; - - let iss = claims.get("iss") + + let iss = claims + .get("iss") .and_then(|v| v.as_str()) .ok_or_else(|| OAuthError::InvalidClient("Missing 'iss' claim".to_string()))?; - let aud = claims.get("aud") + let aud = claims + .get("aud") .ok_or_else(|| OAuthError::InvalidClient("Missing 'aud' claim".to_string()))?; - - let exp = claims.get("exp") + + let exp = claims + .get("exp") .and_then(|v| v.as_i64()) .ok_or_else(|| OAuthError::InvalidClient("Missing 'exp' claim".to_string()))?; - let _jti = claims.get("jti") + let _jti = claims + .get("jti") .and_then(|v| v.as_str()) .ok_or_else(|| OAuthError::InvalidClient("Missing 'jti' claim".to_string()))?; // Validate claims // 1. Subject must equal the client_id if sub != client.client_id { - return Err(OAuthError::InvalidClient("JWT subject does not match client_id".to_string())); + return Err(OAuthError::InvalidClient( + "JWT subject does not match client_id".to_string(), + )); } - // 2. Issuer must equal the client_id + // 2. Issuer must equal the client_id if iss != client.client_id { - return Err(OAuthError::InvalidClient("JWT issuer does not match client_id".to_string())); + return Err(OAuthError::InvalidClient( + "JWT issuer does not match client_id".to_string(), + )); } // 3. Audience must include the token endpoint or current endpoint @@ -883,16 +896,16 @@ pub fn validate_client_assertion( serde_json::Value::String(aud_str) => { aud_str == token_endpoint || current_endpoint.map_or(false, |ep| aud_str == ep) } - serde_json::Value::Array(aud_array) => { - aud_array.iter().any(|v| { - v.as_str() == Some(token_endpoint) || - current_endpoint.map_or(false, |ep| v.as_str() == Some(ep)) - }) - } + serde_json::Value::Array(aud_array) => aud_array.iter().any(|v| { + v.as_str() == Some(token_endpoint) + || current_endpoint.map_or(false, |ep| v.as_str() == Some(ep)) + }), _ => false, }; if !audience_valid { - return Err(OAuthError::InvalidClient("JWT audience does not include token endpoint or current endpoint".to_string())); + return Err(OAuthError::InvalidClient( + "JWT audience does not include token endpoint or current endpoint".to_string(), + )); } // 4. Check expiration @@ -904,68 +917,86 @@ pub fn validate_client_assertion( // Verify JWT signature against client's public key if let Some(ref client_jwks) = client.jwks { // Extract keys from JWK Set - let keys = client_jwks.get("keys") + let keys = client_jwks + .get("keys") .and_then(|k| k.as_array()) .ok_or_else(|| OAuthError::InvalidClient("Invalid JWK Set format".to_string()))?; - + if keys.is_empty() { - return Err(OAuthError::InvalidClient("No public keys found for client".to_string())); + return Err(OAuthError::InvalidClient( + "No public keys found for client".to_string(), + )); } - + // Find the appropriate key for verification let verification_key = if let Some(kid) = kid { // Look for key with matching kid - keys.iter().find(|key| { - key.get("kid").and_then(|v| v.as_str()) == Some(kid) - }).ok_or_else(|| OAuthError::InvalidClient( - format!("No key found with kid: {}", kid) - ))? + keys.iter() + .find(|key| key.get("kid").and_then(|v| v.as_str()) == Some(kid)) + .ok_or_else(|| { + OAuthError::InvalidClient(format!("No key found with kid: {}", kid)) + })? } else { // If no kid, find key with matching algorithm or use first key - keys.iter().find(|key| { - key.get("alg").and_then(|v| v.as_str()) == Some(alg) - }).or(keys.first()) - .ok_or_else(|| OAuthError::InvalidClient("No suitable key found for verification".to_string()))? + keys.iter() + .find(|key| key.get("alg").and_then(|v| v.as_str()) == Some(alg)) + .or(keys.first()) + .ok_or_else(|| { + OAuthError::InvalidClient("No suitable key found for verification".to_string()) + })? }; // Convert JWK to WrappedJsonWebKey for verification let jwk: WrappedJsonWebKey = serde_json::from_value(verification_key.clone()) .map_err(|e| OAuthError::InvalidClient(format!("Invalid JWK format: {}", e)))?; - + // Convert to KeyData for algorithm detection and validation - let key_data = to_key_data(&jwk) - .map_err(|e| OAuthError::InvalidClient(format!("Failed to convert JWK to KeyData: {}", e)))?; - + let key_data = to_key_data(&jwk).map_err(|e| { + OAuthError::InvalidClient(format!("Failed to convert JWK to KeyData: {}", e)) + })?; + // Validate that the algorithm matches the key type let expected_alg = match key_data.key_type() { KeyType::P256Public | KeyType::P256Private => "ES256", KeyType::K256Public | KeyType::K256Private => "ES256K", - _ => return Err(OAuthError::InvalidClient("Unsupported key type".to_string())), + _ => { + return Err(OAuthError::InvalidClient( + "Unsupported key type".to_string(), + )); + } }; - + if alg != expected_alg { - return Err(OAuthError::InvalidClient( - format!("Algorithm mismatch: expected {}, got {}", expected_alg, alg) - )); + return Err(OAuthError::InvalidClient(format!( + "Algorithm mismatch: expected {}, got {}", + expected_alg, alg + ))); } - + // TODO: Implement actual signature verification // This would require creating a JWT verification configuration and using // a JWT library to verify the signature. For now, we validate structure. - tracing::debug!("JWT signature validation placeholder - structure validated for client {}", client.client_id); - + tracing::debug!( + "JWT signature validation placeholder - structure validated for client {}", + client.client_id + ); } else { - return Err(OAuthError::InvalidClient("No public keys configured for private_key_jwt client".to_string())); + return Err(OAuthError::InvalidClient( + "No public keys configured for private_key_jwt client".to_string(), + )); } - + // Check JTI uniqueness to prevent replay attacks // JTI should be unique per client to prevent JWT reuse let jti_key = format!("client_assertion_jti:{}:{}", client.client_id, _jti); - + // Check if this JTI has been used before (simplified check) // In a production implementation, this would use a distributed cache or database // with expiration based on the JWT's exp claim - tracing::debug!("Validating JTI uniqueness for client assertion: {}", jti_key); + tracing::debug!( + "Validating JTI uniqueness for client assertion: {}", + jti_key + ); Ok(sub.to_string()) } @@ -974,7 +1005,7 @@ pub fn validate_client_assertion( mod tests { use super::*; use crate::storage::inmemory::MemoryOAuthStorage; - use crate::storage::traits::{OAuthClientStore, AccessTokenStore}; + use crate::storage::traits::{AccessTokenStore, OAuthClientStore}; #[tokio::test] async fn test_authorization_code_flow() { @@ -1129,16 +1160,18 @@ mod tests { // Create a mock JWT client assertion (normally would be properly signed) let now = Utc::now().timestamp(); let exp = now + 300; // 5 minutes from now - + // Create JWT parts (header.claims.signature) let header = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "typ": "JWT", "alg": "ES256", "kid": "test-key-1" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let claims = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "iss": "test-private-key-jwt-client", @@ -1147,9 +1180,11 @@ mod tests { "iat": now, "exp": exp, "jti": "test-jti-123" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + // Mock signature (would normally be a real ECDSA signature) let signature = BASE64_URL_SAFE_NO_PAD.encode(b"mock_signature_data"); let client_assertion = format!("{}.{}.{}", header, claims, signature); @@ -1158,7 +1193,9 @@ mod tests { client_id: "test-private-key-jwt-client".to_string(), client_secret: None, client_assertion: Some(client_assertion), - client_assertion_type: Some("urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()), + client_assertion_type: Some( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + ), }); let headers = HeaderMap::new(); @@ -1174,7 +1211,11 @@ mod tests { assert_eq!(token_response.scope, Some("read".to_string())); // Verify token is stored correctly - let stored_token = storage.get_token(&token_response.access_token).await.unwrap().unwrap(); + let stored_token = storage + .get_token(&token_response.access_token) + .await + .unwrap() + .unwrap(); assert_eq!(stored_token.client_id, "test-private-key-jwt-client"); assert_eq!(stored_token.user_id, None); // No user for client credentials assert_eq!(stored_token.scope, Some("read".to_string())); @@ -1262,7 +1303,9 @@ mod tests { client_id: "test-private-key-jwt-client".to_string(), client_secret: None, client_assertion: Some("invalid.jwt".to_string()), // Invalid format - client_assertion_type: Some("urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()), + client_assertion_type: Some( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + ), }); let result = auth_server @@ -1280,15 +1323,17 @@ mod tests { // Test 3: Expired JWT let now = Utc::now().timestamp(); let exp = now - 300; // 5 minutes ago (expired) - + let header = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "typ": "JWT", "alg": "ES256", "kid": "test-key-1" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let claims = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "iss": "test-private-key-jwt-client", @@ -1297,9 +1342,11 @@ mod tests { "iat": now, "exp": exp, // Expired "jti": "test-jti-expired" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let signature = BASE64_URL_SAFE_NO_PAD.encode(b"mock_signature_data"); let expired_jwt = format!("{}.{}.{}", header, claims, signature); @@ -1307,7 +1354,9 @@ mod tests { client_id: "test-private-key-jwt-client".to_string(), client_secret: None, client_assertion: Some(expired_jwt), - client_assertion_type: Some("urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()), + client_assertion_type: Some( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + ), }); let result = auth_server @@ -1325,7 +1374,7 @@ mod tests { // Test 4: Wrong issuer let now = Utc::now().timestamp(); let exp = now + 300; - + let claims = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "iss": "wrong-client-id", // Wrong issuer @@ -1334,16 +1383,20 @@ mod tests { "iat": now, "exp": exp, "jti": "test-jti-wrong-issuer" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let wrong_issuer_jwt = format!("{}.{}.{}", header, claims, signature); let client_auth = Some(ClientAuthentication { client_id: "test-private-key-jwt-client".to_string(), client_secret: None, client_assertion: Some(wrong_issuer_jwt), - client_assertion_type: Some("urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()), + client_assertion_type: Some( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + ), }); let result = auth_server @@ -1359,7 +1412,7 @@ mod tests { } } - #[tokio::test] + #[tokio::test] async fn test_private_key_jwt_authorization_code_flow() { let storage = Arc::new(MemoryOAuthStorage::new()); let auth_server = @@ -1370,7 +1423,7 @@ mod tests { "keys": [ { "kty": "EC", - "crv": "P-256", + "crv": "P-256", "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", "use": "sig", @@ -1436,15 +1489,17 @@ mod tests { // Step 2: Token request with private_key_jwt authentication let now = Utc::now().timestamp(); let exp = now + 300; - + let header = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "typ": "JWT", "alg": "ES256", "kid": "test-key-1" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let claims = BASE64_URL_SAFE_NO_PAD.encode( serde_json::json!({ "iss": "test-private-key-jwt-authz-client", @@ -1453,9 +1508,11 @@ mod tests { "iat": now, "exp": exp, "jti": "test-jti-authz-flow" - }).to_string().as_bytes() + }) + .to_string() + .as_bytes(), ); - + let signature = BASE64_URL_SAFE_NO_PAD.encode(b"mock_signature_data"); let client_assertion = format!("{}.{}.{}", header, claims, signature); @@ -1477,7 +1534,9 @@ mod tests { client_id: "test-private-key-jwt-authz-client".to_string(), client_secret: None, client_assertion: Some(client_assertion), - client_assertion_type: Some("urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()), + client_assertion_type: Some( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + ), }); let token_response = auth_server @@ -1491,7 +1550,11 @@ mod tests { assert_eq!(token_response.scope, Some("read".to_string())); // Verify tokens are stored correctly - let stored_token = storage.get_token(&token_response.access_token).await.unwrap().unwrap(); + let stored_token = storage + .get_token(&token_response.access_token) + .await + .unwrap() + .unwrap(); assert_eq!(stored_token.client_id, "test-private-key-jwt-authz-client"); assert_eq!(stored_token.user_id, Some("test-user".to_string())); assert_eq!(stored_token.scope, Some("read".to_string())); diff --git a/src/oauth/clients/registration.rs b/src/oauth/clients/registration.rs index cb0421b..cab93a5 100644 --- a/src/oauth/clients/registration.rs +++ b/src/oauth/clients/registration.rs @@ -89,8 +89,9 @@ impl ClientRegistrationService { // Determine client type // Confidential clients are those with client secrets OR using private_key_jwt auth - let client_type = if client_secret.is_some() || - request.token_endpoint_auth_method.as_ref() == Some(&ClientAuthMethod::PrivateKeyJwt) { + let client_type = if client_secret.is_some() + || request.token_endpoint_auth_method.as_ref() == Some(&ClientAuthMethod::PrivateKeyJwt) + { ClientType::Confidential } else { ClientType::Public @@ -632,34 +633,30 @@ fn extract_client_jwks( // TODO: Implement JWK Set fetching from URI // For now, require inline JWKs Err(ClientRegistrationError::InvalidClientMetadata( - "jwks_uri not yet supported, please provide jwks inline".to_string() - )) - } - (Some(_), Some(_)) => { - Err(ClientRegistrationError::InvalidClientMetadata( - "Cannot specify both jwks and jwks_uri".to_string() - )) - } - (None, None) => { - Err(ClientRegistrationError::InvalidClientMetadata( - "private_key_jwt requires jwks or jwks_uri".to_string() + "jwks_uri not yet supported, please provide jwks inline".to_string(), )) } + (Some(_), Some(_)) => Err(ClientRegistrationError::InvalidClientMetadata( + "Cannot specify both jwks and jwks_uri".to_string(), + )), + (None, None) => Err(ClientRegistrationError::InvalidClientMetadata( + "private_key_jwt requires jwks or jwks_uri".to_string(), + )), } } /// Validate JWK Set format and keys fn validate_jwk_set(jwks: &serde_json::Value) -> Result<(), ClientRegistrationError> { // Check basic JWK Set structure - let keys = jwks.get("keys") - .and_then(|k| k.as_array()) - .ok_or_else(|| ClientRegistrationError::InvalidClientMetadata( - "Invalid JWK Set: missing 'keys' array".to_string() - ))?; + let keys = jwks.get("keys").and_then(|k| k.as_array()).ok_or_else(|| { + ClientRegistrationError::InvalidClientMetadata( + "Invalid JWK Set: missing 'keys' array".to_string(), + ) + })?; if keys.is_empty() { return Err(ClientRegistrationError::InvalidClientMetadata( - "JWK Set cannot be empty".to_string() + "JWK Set cannot be empty".to_string(), )); } @@ -676,58 +673,66 @@ fn validate_jwk(jwk: &serde_json::Value, index: usize) -> Result<(), ClientRegis let error_prefix = format!("Invalid JWK at index {}", index); // Check required fields - let kty = jwk.get("kty") - .and_then(|v| v.as_str()) - .ok_or_else(|| ClientRegistrationError::InvalidClientMetadata( - format!("{}: missing 'kty' field", error_prefix) - ))?; + let kty = jwk.get("kty").and_then(|v| v.as_str()).ok_or_else(|| { + ClientRegistrationError::InvalidClientMetadata(format!( + "{}: missing 'kty' field", + error_prefix + )) + })?; - let alg = jwk.get("alg") - .and_then(|v| v.as_str()); + let alg = jwk.get("alg").and_then(|v| v.as_str()); // Validate key type and algorithm match kty { "EC" => { - let crv = jwk.get("crv") - .and_then(|v| v.as_str()) - .ok_or_else(|| ClientRegistrationError::InvalidClientMetadata( - format!("{}: EC key missing 'crv' field", error_prefix) - ))?; + let crv = jwk.get("crv").and_then(|v| v.as_str()).ok_or_else(|| { + ClientRegistrationError::InvalidClientMetadata(format!( + "{}: EC key missing 'crv' field", + error_prefix + )) + })?; // Validate curve and algorithm compatibility match (crv, alg) { ("P-256", Some("ES256")) | ("P-256", None) => {} ("secp256k1", Some("ES256K")) | ("secp256k1", None) => {} - _ => return Err(ClientRegistrationError::InvalidClientMetadata( - format!("{}: unsupported curve/algorithm combination", error_prefix) - )) + _ => { + return Err(ClientRegistrationError::InvalidClientMetadata(format!( + "{}: unsupported curve/algorithm combination", + error_prefix + ))); + } } // Check required EC key components if jwk.get("x").is_none() || jwk.get("y").is_none() { - return Err(ClientRegistrationError::InvalidClientMetadata( - format!("{}: EC key missing x/y coordinates", error_prefix) - )); + return Err(ClientRegistrationError::InvalidClientMetadata(format!( + "{}: EC key missing x/y coordinates", + error_prefix + ))); } } "RSA" => { - return Err(ClientRegistrationError::InvalidClientMetadata( - format!("{}: RSA keys not supported for private_key_jwt", error_prefix) - )); + return Err(ClientRegistrationError::InvalidClientMetadata(format!( + "{}: RSA keys not supported for private_key_jwt", + error_prefix + ))); } _ => { - return Err(ClientRegistrationError::InvalidClientMetadata( - format!("{}: unsupported key type '{}'", error_prefix, kty) - )); + return Err(ClientRegistrationError::InvalidClientMetadata(format!( + "{}: unsupported key type '{}'", + error_prefix, kty + ))); } } // Key usage should be 'sig' for signing if let Some(use_val) = jwk.get("use").and_then(|v| v.as_str()) { if use_val != "sig" { - return Err(ClientRegistrationError::InvalidClientMetadata( - format!("{}: key use must be 'sig' for JWT signing", error_prefix) - )); + return Err(ClientRegistrationError::InvalidClientMetadata(format!( + "{}: key use must be 'sig' for JWT signing", + error_prefix + ))); } } diff --git a/src/oauth/utils_app_password.rs b/src/oauth/utils_app_password.rs index 0d76c04..ed35d09 100644 --- a/src/oauth/utils_app_password.rs +++ b/src/oauth/utils_app_password.rs @@ -343,7 +343,7 @@ mod tests { use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; use crate::storage::traits::AppPassword; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -352,8 +352,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/oauth/utils_atprotocol_oauth.rs b/src/oauth/utils_atprotocol_oauth.rs index d76fded..256997e 100644 --- a/src/oauth/utils_atprotocol_oauth.rs +++ b/src/oauth/utils_atprotocol_oauth.rs @@ -395,7 +395,7 @@ mod tests { use crate::oauth::DPoPNonceGenerator; use crate::storage::SimpleKeyProvider; use crate::storage::inmemory::MemoryOAuthStorage; - use atproto_identity::{resolve::create_resolver, storage_lru::LruDidDocumentStorage}; + use atproto_identity::{resolve::HickoryDnsResolver, storage_lru::LruDidDocumentStorage}; use atproto_oauth::storage_lru::LruOAuthRequestStorage; use std::{num::NonZeroUsize, sync::Arc}; @@ -404,8 +404,8 @@ mod tests { let http_client = reqwest::Client::new(); let dns_nameservers = vec![]; - let dns_resolver = create_resolver(&dns_nameservers); - let identity_resolver = atproto_identity::resolve::IdentityResolver(Arc::new( + let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&dns_nameservers)); + let identity_resolver = atproto_identity::resolve::SharedIdentityResolver(Arc::new( atproto_identity::resolve::InnerIdentityResolver { http_client: http_client.clone(), dns_resolver, diff --git a/src/storage/postgres/did_documents.rs b/src/storage/postgres/did_documents.rs index 4b40fbc..efd015f 100644 --- a/src/storage/postgres/did_documents.rs +++ b/src/storage/postgres/did_documents.rs @@ -91,6 +91,7 @@ mod tests { let storage = PostgresDidDocumentStorage::new(pool); let document = Document { + context: vec![], id: "did:plc:test123".to_string(), also_known_as: vec!["at://test.bsky.social".to_string()], service: vec![Service { @@ -138,6 +139,7 @@ mod tests { let storage = PostgresDidDocumentStorage::new(pool); let mut document = Document { + context: vec![], id: "did:plc:test456".to_string(), also_known_as: vec!["at://original.bsky.social".to_string()], service: vec![], diff --git a/src/storage/sqlite/oauth_clients.rs b/src/storage/sqlite/oauth_clients.rs index 4b9ac95..c62e3cb 100644 --- a/src/storage/sqlite/oauth_clients.rs +++ b/src/storage/sqlite/oauth_clients.rs @@ -256,7 +256,8 @@ impl SqliteOAuthClientStore { refresh_token_expiration, require_redirect_exact, registration_access_token, - jwks: row.try_get::, _>("jwks") + jwks: row + .try_get::, _>("jwks") .ok() .flatten() .and_then(|s| serde_json::from_str(&s).ok()), @@ -313,7 +314,12 @@ impl OAuthClientStore for SqliteOAuthClientStore { 0i64 }) .bind(&client.registration_access_token) - .bind(&client.jwks.as_ref().map(|j| serde_json::to_string(j).unwrap_or_default())) + .bind( + &client + .jwks + .as_ref() + .map(|j| serde_json::to_string(j).unwrap_or_default()), + ) .execute(&self.pool) .await .map_err(|e| StorageError::DatabaseError(e.to_string()))?;