Skip to content

Commit f1b65e9

Browse files
committed
feat(pubky): add PubkyClient::auth_request and send_auth_token
1 parent a7f70cc commit f1b65e9

File tree

10 files changed

+239
-60
lines changed

10 files changed

+239
-60
lines changed

examples/authz/authenticator/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async fn main() -> Result<()> {
6161
}
6262

6363
for cap in &required_capabilities {
64-
println!(" {} : {:?}", cap.resource, cap.abilities);
64+
println!(" {} : {:?}", cap.scope, cap.abilities);
6565
}
6666

6767
// === Consent form ===

pubky-common/src/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl AuthToken {
6464
&self.capabilities.0
6565
}
6666

67-
fn verify(bytes: &[u8]) -> Result<Self, Error> {
67+
pub fn verify(bytes: &[u8]) -> Result<Self, Error> {
6868
if bytes[75] > CURRENT_VERSION {
6969
return Err(Error::UnknownVersion);
7070
}

pubky-common/src/capabilities.rs

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ use serde::{Deserialize, Serialize};
44

55
#[derive(Debug, Clone, PartialEq, Eq)]
66
pub struct Capability {
7-
pub resource: String,
7+
pub scope: String,
88
pub abilities: Vec<Ability>,
99
}
1010

1111
impl Capability {
1212
/// Create a root [Capability] at the `/` path with all the available [PubkyAbility]
1313
pub fn root() -> Self {
1414
Capability {
15-
resource: "/".to_string(),
15+
scope: "/".to_string(),
1616
abilities: vec![Ability::Read, Ability::Write],
1717
}
1818
}
1919
}
2020

2121
#[derive(Debug, Clone, PartialEq, Eq)]
2222
pub enum Ability {
23-
/// Can read the resource at the specified path (GET requests).
23+
/// Can read the scope at the specified path (GET requests).
2424
Read,
25-
/// Can write to the resource at the specified path (PUT/POST/DELETE requests).
25+
/// Can write to the scope at the specified path (PUT/POST/DELETE requests).
2626
Write,
2727
/// Unknown ability
2828
Unknown(char),
@@ -55,7 +55,7 @@ impl Display for Capability {
5555
write!(
5656
f,
5757
"{}:{}",
58-
self.resource,
58+
self.scope,
5959
self.abilities.iter().map(char::from).collect::<String>()
6060
)
6161
}
@@ -78,7 +78,7 @@ impl TryFrom<&str> for Capability {
7878
}
7979

8080
if !value.starts_with('/') {
81-
return Err(Error::InvalidResource);
81+
return Err(Error::InvalidScope);
8282
}
8383

8484
let abilities_str = value.rsplit(':').next().unwrap_or("");
@@ -96,12 +96,9 @@ impl TryFrom<&str> for Capability {
9696
}
9797
}
9898

99-
let resource = value[0..value.len() - abilities_str.len() - 1].to_string();
99+
let scope = value[0..value.len() - abilities_str.len() - 1].to_string();
100100

101-
Ok(Capability {
102-
resource,
103-
abilities,
104-
})
101+
Ok(Capability { scope, abilities })
105102
}
106103
}
107104

@@ -129,12 +126,14 @@ impl<'de> Deserialize<'de> for Capability {
129126

130127
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
131128
pub enum Error {
132-
#[error("Capability: Invalid resource path: does not start with `/`")]
133-
InvalidResource,
134-
#[error("Capability: Invalid format should be <resource>:<abilities>")]
129+
#[error("Capability: Invalid scope: does not start with `/`")]
130+
InvalidScope,
131+
#[error("Capability: Invalid format should be <scope>:<abilities>")]
135132
InvalidFormat,
136133
#[error("Capability: Invalid Ability")]
137134
InvalidAbility,
135+
#[error("Capabilities: Invalid capabilities format")]
136+
InvalidCapabilities,
138137
}
139138

140139
#[derive(Clone, Default, Debug, PartialEq, Eq)]
@@ -160,19 +159,41 @@ impl From<Capabilities> for Vec<Capability> {
160159
}
161160
}
162161

163-
impl Serialize for Capabilities {
164-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165-
where
166-
S: serde::Serializer,
167-
{
162+
impl TryFrom<&str> for Capabilities {
163+
type Error = Error;
164+
165+
fn try_from(value: &str) -> Result<Self, Self::Error> {
166+
let mut caps = vec![];
167+
168+
for s in value.split(',') {
169+
if let Ok(cap) = Capability::try_from(s) {
170+
caps.push(cap);
171+
};
172+
}
173+
174+
Ok(Capabilities(caps))
175+
}
176+
}
177+
178+
impl Display for Capabilities {
179+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168180
let string = self
169181
.0
170182
.iter()
171183
.map(|c| c.to_string())
172184
.collect::<Vec<_>>()
173185
.join(",");
174186

175-
string.serialize(serializer)
187+
write!(f, "{}", string)
188+
}
189+
}
190+
191+
impl Serialize for Capabilities {
192+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
193+
where
194+
S: serde::Serializer,
195+
{
196+
self.to_string().serialize(serializer)
176197
}
177198
}
178199

@@ -202,7 +223,7 @@ mod tests {
202223
#[test]
203224
fn pubky_caps() {
204225
let cap = Capability {
205-
resource: "/pub/pubky.app/".to_string(),
226+
scope: "/pub/pubky.app/".to_string(),
206227
abilities: vec![Ability::Read, Ability::Write],
207228
};
208229

pubky-common/src/session.rs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use pkarr::PublicKey;
12
use postcard::{from_bytes, to_allocvec};
23
use serde::{Deserialize, Serialize};
34

@@ -9,9 +10,10 @@ use crate::{auth::AuthToken, capabilities::Capability, timestamp::Timestamp};
910
// TODO: add IP address?
1011
// TODO: use https://crates.io/crates/user-agent-parser to parse the session
1112
// and get more informations from the user-agent.
12-
#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)]
13+
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
1314
pub struct Session {
1415
pub version: usize,
16+
pub pubky: PublicKey,
1517
pub created_at: u64,
1618
/// User specified name, defaults to the user-agent.
1719
pub name: String,
@@ -21,18 +23,14 @@ pub struct Session {
2123

2224
impl Session {
2325
pub fn new(token: &AuthToken, user_agent: Option<String>) -> Self {
24-
let mut session = Self {
26+
Self {
27+
version: 0,
28+
pubky: token.pubky().to_owned(),
2529
created_at: Timestamp::now().into_inner(),
26-
..Default::default()
27-
};
28-
29-
session.set_capabilities(token.capabilities().to_vec());
30-
31-
if let Some(user_agent) = user_agent {
32-
session.set_user_agent(user_agent);
30+
capabilities: token.capabilities().to_vec(),
31+
user_agent: user_agent.as_deref().unwrap_or("").to_string(),
32+
name: user_agent.as_deref().unwrap_or("").to_string(),
3333
}
34-
35-
session
3634
}
3735

3836
// === Setters ===
@@ -82,21 +80,33 @@ pub enum Error {
8280

8381
#[cfg(test)]
8482
mod tests {
83+
use crate::crypto::Keypair;
84+
8585
use super::*;
8686

8787
#[test]
8888
fn serialize() {
89+
let keypair = Keypair::from_secret_key(&[0; 32]);
90+
let pubky = keypair.public_key();
91+
8992
let session = Session {
9093
user_agent: "foo".to_string(),
9194
capabilities: vec![Capability::root()],
92-
..Default::default()
95+
created_at: 0,
96+
pubky,
97+
version: 0,
98+
name: "".to_string(),
9399
};
94100

95101
let serialized = session.serialize();
96102

97103
assert_eq!(
98104
serialized,
99-
[0, 0, 0, 3, 102, 111, 111, 1, 4, 47, 58, 114, 119]
105+
[
106+
0, 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13, 115, 101,
107+
50, 21, 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41, 0, 0, 3, 102,
108+
111, 111, 1, 4, 47, 58, 114, 119
109+
]
100110
);
101111

102112
let deseiralized = Session::deserialize(&serialized).unwrap();

pubky-homeserver/src/routes/auth.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,13 @@ pub async fn signin(
113113

114114
let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes(16));
115115

116-
state.db.tables.sessions.put(
117-
&mut wtxn,
118-
&session_secret,
119-
&Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize(),
120-
)?;
116+
let session = Session::new(&token, user_agent.map(|ua| ua.to_string())).serialize();
117+
118+
state
119+
.db
120+
.tables
121+
.sessions
122+
.put(&mut wtxn, &session_secret, &session)?;
121123

122124
let mut cookie = Cookie::new(public_key.to_string(), session_secret);
123125

@@ -132,7 +134,5 @@ pub async fn signin(
132134

133135
wtxn.commit()?;
134136

135-
// TODO: return session to save extra call?
136-
137-
Ok(())
137+
Ok(session)
138138
}

pubky/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ thiserror = "1.0.62"
1515
wasm-bindgen = "0.2.92"
1616
url = "2.5.2"
1717
bytes = "^1.7.1"
18+
base64 = "0.22.1"
1819

1920
pubky-common = { version = "0.1.0", path = "../pubky-common" }
2021
pkarr = { workspace = true, features = ["async"] }
21-
base64 = "0.22.1"
22+
tokio = { version = "1.37.0", features = ["full"] }
2223

2324
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
2425
reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false }

pubky/src/native.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl PubkyClient {
128128
}
129129

130130
/// Signin to a homeserver.
131-
pub async fn signin(&self, keypair: &Keypair) -> Result<()> {
131+
pub async fn signin(&self, keypair: &Keypair) -> Result<Session> {
132132
self.inner_signin(keypair).await
133133
}
134134

@@ -192,6 +192,6 @@ impl PubkyClient {
192192
self.http.request(method, url)
193193
}
194194

195-
pub(crate) fn store_session(&self, _: Response) {}
195+
pub(crate) fn store_session(&self, _: &Response) {}
196196
pub(crate) fn remove_session(&self, _: &PublicKey) {}
197197
}

0 commit comments

Comments
 (0)