Skip to content

Commit 078cb0d

Browse files
committed
wip
1 parent c902fa3 commit 078cb0d

File tree

11 files changed

+399
-68
lines changed

11 files changed

+399
-68
lines changed

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

atrium-oauth/oauth-client/Cargo.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ keywords = ["atproto", "bluesky", "oauth"]
1414
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1515

1616
[dependencies]
17-
atrium-api = { workspace = true, default-features = false }
17+
atrium-api = { workspace = true, default-features = true }
1818
atrium-common.workspace = true
1919
atrium-identity.workspace = true
2020
atrium-xrpc.workspace = true
2121
base64.workspace = true
2222
chrono.workspace = true
2323
ecdsa = { workspace = true, features = ["signing"] }
2424
elliptic-curve.workspace = true
25+
futures.workspace = true
2526
jose-jwa.workspace = true
2627
jose-jwk = { workspace = true, features = ["p256"] }
2728
p256 = { workspace = true, features = ["ecdsa"] }
@@ -33,11 +34,14 @@ serde_json.workspace = true
3334
sha2.workspace = true
3435
thiserror.workspace = true
3536
trait-variant.workspace = true
36-
37-
[dev-dependencies]
37+
# [dev-dependencies]
3838
hickory-resolver.workspace = true
3939
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
4040

4141
[features]
4242
default = ["default-client"]
4343
default-client = ["reqwest/default-tls"]
44+
45+
[[bin]]
46+
name = "client"
47+
path = "src/client.rs"
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use atrium_api::{agent::Agent, chat::bsky::convo::get_messages, types::string::Datetime};
2+
use atrium_common::store::memory::MemoryMapStore;
3+
use atrium_identity::{
4+
did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL},
5+
handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig},
6+
};
7+
use atrium_oauth_client::store::session::Session;
8+
use atrium_oauth_client::store::state::InternalStateData;
9+
use atrium_oauth_client::store::state::MemoryStateStore;
10+
use atrium_oauth_client::AtprotoLocalhostClientMetadata;
11+
use atrium_oauth_client::AuthorizeOptions;
12+
use atrium_oauth_client::DefaultHttpClient;
13+
use atrium_oauth_client::OAuthClient;
14+
use atrium_oauth_client::OAuthClientConfig;
15+
use atrium_oauth_client::OAuthResolverConfig;
16+
use jose_jwk::JwkSet;
17+
use std::sync::Arc;
18+
19+
struct HickoryDnsTxtResolver {
20+
resolver: hickory_resolver::TokioAsyncResolver,
21+
}
22+
23+
impl Default for HickoryDnsTxtResolver {
24+
fn default() -> Self {
25+
Self {
26+
resolver: hickory_resolver::TokioAsyncResolver::tokio_from_system_conf()
27+
.expect("failed to create resolver"),
28+
}
29+
}
30+
}
31+
32+
impl atrium_identity::handle::DnsTxtResolver for HickoryDnsTxtResolver {
33+
async fn resolve(
34+
&self,
35+
query: &str,
36+
) -> core::result::Result<Vec<String>, Box<dyn std::error::Error + Send + Sync + 'static>> {
37+
Ok(self.resolver.txt_lookup(query).await?.iter().map(|txt| txt.to_string()).collect())
38+
}
39+
}
40+
41+
fn keyset() -> JwkSet {
42+
serde_json::from_value(serde_json::json!({
43+
"keys": [
44+
{
45+
"kty": "EC",
46+
"crv": "P-256",
47+
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
48+
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
49+
"use": "enc",
50+
"kid": "some-ec-kid"
51+
},
52+
{
53+
"kty": "RSA",
54+
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtV\
55+
T86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5\
56+
JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMic\
57+
AtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bF\
58+
TWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-\
59+
kEgU8awapJzKnqDKgw",
60+
"e": "AQAB",
61+
"alg": "RS256",
62+
"kid": "some-rsa-kid"
63+
}
64+
]
65+
}))
66+
.unwrap()
67+
}
68+
69+
fn config() -> OAuthClientConfig<
70+
MemoryMapStore<String, InternalStateData>,
71+
MemoryMapStore<String, Session>,
72+
AtprotoLocalhostClientMetadata,
73+
CommonDidResolver<DefaultHttpClient>,
74+
AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>,
75+
> {
76+
let http_client = Arc::new(DefaultHttpClient::default());
77+
78+
OAuthClientConfig {
79+
client_metadata: AtprotoLocalhostClientMetadata {
80+
redirect_uris: vec!["http://127.0.0.1".to_string()],
81+
},
82+
// keys: Some(keyset().keys).filter(|_| true),
83+
keys: None,
84+
resolver: OAuthResolverConfig {
85+
did_resolver: CommonDidResolver::new(CommonDidResolverConfig {
86+
plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
87+
http_client: http_client.clone(),
88+
}),
89+
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
90+
dns_txt_resolver: HickoryDnsTxtResolver::default(),
91+
http_client: http_client.clone(),
92+
}),
93+
authorization_server_metadata: Default::default(),
94+
protected_resource_metadata: Default::default(),
95+
},
96+
state_store: MemoryStateStore::default(),
97+
session_store: MemoryMapStore::default(),
98+
}
99+
}
100+
101+
#[tokio::main]
102+
async fn main() {
103+
use std::io::{stdin, stdout, BufRead, Write};
104+
105+
use atrium_xrpc::http::Uri;
106+
107+
let client = OAuthClient::new(config()).expect("OAuthClient");
108+
109+
let handle = std::env::var("HANDLE").unwrap_or(String::from("https://bsky.social"));
110+
let options =
111+
AuthorizeOptions { scopes: Some(vec!["atproto".to_string()]), ..Default::default() };
112+
113+
println!(
114+
"Authorization URL: {}",
115+
client.authorize(handle, options).await.expect("Authorization")
116+
);
117+
118+
print!("Redirect url: ");
119+
stdout().lock().flush().unwrap();
120+
let mut url = String::new();
121+
stdin().lock().read_line(&mut url).unwrap();
122+
123+
let uri = url.trim().parse::<Uri>().expect("Uri");
124+
let params = serde_html_form::from_str(uri.query().unwrap()).expect("serde_html_form");
125+
let session = client.callback(params).await.expect("callback");
126+
127+
let inner = session.get_session(true).await.expect("get_session");
128+
let expires_at = inner.token_set.expires_at.expect("expires_at");
129+
println!(
130+
"expires_at: {:?}",
131+
expires_at.as_ref().signed_duration_since(Datetime::now().as_ref()).num_seconds()
132+
);
133+
134+
let agent = Agent::new(session);
135+
let get_session = agent.api.chat.bsky.convo.get_messages(
136+
get_messages::ParametersData { convo_id: "convo".to_owned(), cursor: None, limit: None }
137+
.into(),
138+
).await;
139+
140+
println!("get_session: {:?}", get_session);
141+
}

atrium-oauth/oauth-client/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum Error {
1616
Callback(String),
1717
#[error("state store error: {0:?}")]
1818
StateStore(Box<dyn std::error::Error + Send + Sync + 'static>),
19+
#[error("session error: {0:?}")]
20+
Session(#[from] crate::oauth_session::Error),
1921
}
2022

2123
pub type Result<T> = core::result::Result<T, Error>;

atrium-oauth/oauth-client/src/http_client/dpop.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use jose_jwa::{Algorithm, Signing};
1212
use jose_jwk::{crypto, EcCurves, Jwk, Key};
1313
use rand::rngs::SmallRng;
1414
use rand::{RngCore, SeedableRng};
15+
use reqwest::header::HeaderValue;
1516
use serde::Deserialize;
1617
use std::sync::Arc;
1718
use thiserror::Error;
@@ -95,16 +96,19 @@ impl<T> DpopClient<T> {
9596
_ => unimplemented!(),
9697
}
9798
}
98-
fn is_use_dpop_nonce_error(&self, response: &Response<Vec<u8>>) -> bool {
99-
// is auth server?
100-
if response.status() == 400 {
101-
if let Ok(res) = serde_json::from_slice::<ErrorResponse>(response.body()) {
102-
return res.error == "use_dpop_nonce";
103-
};
104-
}
105-
// is resource server?
99+
fn is_use_dpop_nonce_error(&self, response: &Response<Vec<u8>>, is_auth_server: bool) -> bool {
100+
let status = response.status();
101+
let www_auth = response.headers().get("WWW-Authenticate");
102+
let body = serde_json::from_slice::<ErrorResponse>(response.body());
103+
104+
let header = www_auth.map(HeaderValue::to_str).and_then(core::result::Result::ok);
105+
let suffix = header.and_then(|s| s.strip_prefix("DPoP "));
106106

107-
false
107+
suffix.map_or(false, |value| {
108+
!is_auth_server && status == 401 && value.contains("error=\"use_dpop_nonce\"")
109+
}) || body.map_or(false, |value| {
110+
is_auth_server && status == 400 && value.error == "use_dpop_nonce"
111+
})
108112
}
109113
// https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
110114
fn generate_jti() -> String {
@@ -129,11 +133,23 @@ where
129133
let htm = request.method().to_string();
130134
let htu = uri.to_string();
131135

136+
let is_auth_server = uri.path().starts_with("/oauth");
137+
138+
dbg!(&uri, &nonce_key, &htm, &htu);
139+
132140
let init_nonce = self.nonces.get(&nonce_key).await?;
133141
let init_proof = self.build_proof(htm.clone(), htu.clone(), init_nonce.clone())?;
134142
request.headers_mut().insert("DPoP", init_proof.parse()?);
143+
144+
145+
let _request = request.clone();
146+
dbg!(&_request.headers());
147+
135148
let response = self.inner.send_http(request.clone()).await?;
136149

150+
let _response = response.clone();
151+
dbg!(std::str::from_utf8(&_response.into_body()));
152+
137153
let next_nonce =
138154
response.headers().get("DPoP-Nonce").and_then(|v| v.to_str().ok()).map(String::from);
139155
match &next_nonce {
@@ -148,7 +164,7 @@ where
148164
}
149165
}
150166

151-
if !self.is_use_dpop_nonce_error(&response) {
167+
if !self.is_use_dpop_nonce_error(&response, is_auth_server) {
152168
return Ok(response);
153169
}
154170
let next_proof = self.build_proof(htm, htu, next_nonce)?;

atrium-oauth/oauth-client/src/oauth_client.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,7 @@ where
187187
prompt: options.prompt.map(String::from),
188188
};
189189
if metadata.pushed_authorization_request_endpoint.is_some() {
190-
let server = OAuthServerAgent::new(
191-
dpop_key,
192-
metadata.clone(),
193-
self.client_metadata.clone(),
194-
self.resolver.clone(),
195-
self.http_client.clone(),
196-
self.keyset.clone(),
197-
)?;
190+
let server = self.server_from_metadata(metadata.clone(), dpop_key)?;
198191
let par_response = server
199192
.request::<OAuthPusehedAuthorizationRequestResponse>(
200193
OAuthRequest::PushedAuthorizationRequest(parameters),
@@ -258,7 +251,7 @@ where
258251
Ok(OAuthSession::new(
259252
token_set.sub.parse().unwrap(),
260253
self.server_from_metadata(metadata.clone(), state.dpop_key.clone()).unwrap(),
261-
Arc::new(EndpointStore::new(store, String::new())),
254+
store,
262255
))
263256
}
264257
fn generate_dpop_key(metadata: &OAuthAuthorizationServerMetadata) -> Option<Key> {

0 commit comments

Comments
 (0)