From db006612ef8d64768e86f16f787cb184632f421e Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 7 Sep 2024 11:35:24 +0300 Subject: [PATCH] feat(js): return Session class from auth methods --- Cargo.lock | 29 +++++----- .../3rd-party-app/src/pubky-auth-widget.js | 34 +++-------- pubky-common/src/session.rs | 22 ++++++-- pubky-homeserver/src/routes/public.rs | 4 +- pubky/pkg/README.md | 34 +++++++++-- pubky/pkg/test/auth.js | 39 +++++++++++-- pubky/pkg/test/public.js | 22 +++----- pubky/src/native.rs | 2 +- pubky/src/shared/auth.rs | 16 +++--- pubky/src/wasm.rs | 56 +++++++++++-------- pubky/src/wasm/keys.rs | 6 ++ pubky/src/wasm/session.rs | 21 +++++++ 12 files changed, 182 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abf383f..a712105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,9 +1110,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2540,19 +2540,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -2577,9 +2578,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2587,9 +2588,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -2600,15 +2601,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/examples/authz/3rd-party-app/src/pubky-auth-widget.js b/examples/authz/3rd-party-app/src/pubky-auth-widget.js index 0f4f084..2070f82 100644 --- a/examples/authz/3rd-party-app/src/pubky-auth-widget.js +++ b/examples/authz/3rd-party-app/src/pubky-auth-widget.js @@ -55,12 +55,19 @@ export class PubkyAuthWidget extends LitElement { let [url, promise] = this.pubkyClient.authRequest(this.relay || DEFAULT_HTTP_RELAY, this.caps); - promise.then(x => { - console.log({ x }) + promise.then(session => { + console.log({ id: session.pubky().z32(), capabilities: session.capabilities() }) + alert(`Successfully signed in to ${session.pubky().z32()} with capabilities: ${session.capabilities().join(",")}`) }).catch(e => { console.error(e) }) + // let keypair = pubky.Keypair.random(); + // const Homeserver = pubky.PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + // this.pubkyClient.signup(keypair, Homeserver).then(() => { + // this.pubkyClient.sendAuthToken(keypair, url) + // }) + this.authUrl = url } @@ -108,29 +115,6 @@ export class PubkyAuthWidget extends LitElement { this.open = !this.open } - async _onCallback(response) { - try { - // Check if the response is ok (status code 200-299) - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - // Convert the response to an ArrayBuffer - const arrayBuffer = await response.arrayBuffer(); - - // Create a Uint8Array from the ArrayBuffer - const authToken = new Uint8Array(arrayBuffer); - - let publicKey = await this.pubkyClient.thirdPartySignin(authToken, this.secret) - - let session = await this.pubkyClient.session(publicKey); - - alert(`Succssfully signed in as ${publicKey.z32()}`) - } catch (error) { - console.error('PubkyAuthWidget: Failed to read incoming AuthToken', error); - } - } - async _copyToClipboard() { try { await navigator.clipboard.writeText(this.authUrl); diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 6a718ac..5ce64d0 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -12,13 +12,13 @@ use crate::{auth::AuthToken, capabilities::Capability, timestamp::Timestamp}; // and get more informations from the user-agent. #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Session { - pub version: usize, - pub pubky: PublicKey, - pub created_at: u64, + version: usize, + pubky: PublicKey, + created_at: u64, /// User specified name, defaults to the user-agent. - pub name: String, - pub user_agent: String, - pub capabilities: Vec, + name: String, + user_agent: String, + capabilities: Vec, } impl Session { @@ -33,6 +33,16 @@ impl Session { } } + // === Getters === + + pub fn pubky(&self) -> &PublicKey { + &self.pubky + } + + pub fn capabilities(&self) -> &Vec { + &self.capabilities + } + // === Setters === pub fn set_user_agent(&mut self, user_agent: String) -> &mut Self { diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index 2d417dd..5c5da84 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -148,8 +148,8 @@ fn authorize( .get_session(cookies, public_key)? .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; - if session.pubky == *public_key - && session.capabilities.iter().any(|cap| { + if session.pubky() == public_key + && session.capabilities().iter().any(|cap| { path.starts_with(&cap.scope[1..]) && cap .abilities diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 9fd9763..2228266 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -84,7 +84,7 @@ let session = await client.signin(keypair) - keypair: An instance of [Keypair](#keypair). Returns: -- session: An instance of [Session](#session). +- An instance of [Session](#session). #### signout ```js @@ -102,8 +102,7 @@ let session = await sessionPromise; ``` Sign in to a user's Homeserver, without access to their [Keypair](#keypair), nor even [PublicKey](#publickey), -instead request permissions (showing the user pubkyauthUrl), and await a Session -after the user consenting to that request. +instead request permissions (showing the user pubkyauthUrl), and await a Session after the user consenting to that request. - relay: A URL to an [HTTP relay](https://httprelay.io/features/link/) endpoint. - capabilities: A list of capabilities required for the app for example `/pub/pubky.app/:rw,/pub/example.com/:r`. @@ -112,12 +111,21 @@ Returns: - pubkyauthUrl: A url to show to the user to scan or paste into an Authenticator app holding the user [Keypair](#keypair) - sessionPromise: A promise that resolves into a [Session](#session) on success. -#### session +#### sendAuthToken +```js +await client.sendAuthToken(keypair, pubkyauthUrl); +``` +Consenting to authentication or authorization according to the required capabilities in the `pubkyauthUrl` , and sign and send an auth token to the requester. + +- keypair: An instance of [KeyPair](#keypair) +- pubkyauthUrl: A string `pubkyauth://` url + +#### session {#session-method} ```js let session = await client.session(publicKey) ``` - publicKey: An instance of [PublicKey](#publickey). -- Returns: A session object if signed in, or undefined if not. +- Returns: A [Session](#session) object if signed in, or undefined if not. #### put ```js @@ -166,7 +174,7 @@ let keypair = Keypair.fromSecretKey(secretKey) - Returns: A new Keypair. -#### publicKey +#### publicKey {#publickey-method} ```js let publicKey = keypair.publicKey() ``` @@ -194,6 +202,20 @@ let pubky = publicKey.z32(); ``` Returns: The z-base-32 encoded string representation of the PublicKey. +### Session + +#### pubky +```js +let pubky = session.pubky(); +``` +Returns an instance of [PublicKey](#publickey) + +#### capabilities +```js +let capabilities = session.capabilities(); +``` +Returns an array of capabilities, for example `["/pub/pubky.app/:rw"]` + ### Helper functions #### createRecoveryFile diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 0151004..2207946 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -2,14 +2,15 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.cjs' +const Homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + test('auth', async (t) => { const client = PubkyClient.testnet(); const keypair = Keypair.random() const publicKey = keypair.publicKey() - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') - await client.signup(keypair, homeserver) + await client.signup(keypair, Homeserver) const session = await client.session(publicKey) t.ok(session, "signup") @@ -29,6 +30,34 @@ test('auth', async (t) => { } }) -// TODO: test end to end -// TODO: test invalid inputs -test.skip("3rd party signin") +test("3rd party signin", async (t) => { + let keypair = Keypair.random(); + let pubky = keypair.publicKey().z32(); + + // Third party app side + let capabilities = "/pub/pubky.app/:rw,/pub/foo.bar/file:r"; + let client = PubkyClient.testnet(); + let [pubkyauth_url, pubkyauthResponse] = client + .authRequest("https://demo.httprelay.io/link", capabilities); + + if (globalThis.document) { + // Skip `sendAuthToken` in browser + // TODO: figure out why does it fail in browser unit tests + // but not in real browser (check pubky-auth-widget.js commented part) + return + } + + // Authenticator side + { + let client = PubkyClient.testnet(); + + await client.signup(keypair, Homeserver); + + await client.sendAuthToken(keypair, pubkyauth_url) + } + + let session = await pubkyauthResponse; + + t.is(session.pubky().z32(), pubky) + t.deepEqual(session.capabilities(), capabilities.split(',')) +}) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 6355ee4..ec30bb2 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -2,13 +2,14 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.cjs' +const Homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + test('public: put/get', async (t) => { const client = PubkyClient.testnet(); const keypair = Keypair.random(); - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); - await client.signup(keypair, homeserver); + await client.signup(keypair, Homeserver); const publicKey = keypair.publicKey(); @@ -46,8 +47,7 @@ test("not found", async (t) => { const keypair = Keypair.random(); - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); - await client.signup(keypair, homeserver); + await client.signup(keypair, Homeserver); const publicKey = keypair.publicKey(); @@ -64,8 +64,7 @@ test("unauthorized", async (t) => { const keypair = Keypair.random() const publicKey = keypair.publicKey() - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') - await client.signup(keypair, homeserver) + await client.signup(keypair, Homeserver) const session = await client.session(publicKey) t.ok(session, "signup") @@ -92,8 +91,7 @@ test("forbidden", async (t) => { const keypair = Keypair.random() const publicKey = keypair.publicKey() - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') - await client.signup(keypair, homeserver) + await client.signup(keypair, Homeserver) const session = await client.session(publicKey) t.ok(session, "signup") @@ -119,8 +117,7 @@ test("list", async (t) => { const publicKey = keypair.publicKey() const pubky = publicKey.z32() - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') - await client.signup(keypair, homeserver) + await client.signup(keypair, Homeserver) @@ -251,10 +248,7 @@ test('list shallow', async (t) => { const publicKey = keypair.publicKey() const pubky = publicKey.z32() - const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') - await client.signup(keypair, homeserver) - - + await client.signup(keypair, Homeserver) let urls = [ `pubky://${pubky}/pub/a.com/a.txt`, diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 64b334b..ba0f086 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -118,7 +118,7 @@ impl PubkyClient { /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" - pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> { + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result { self.inner_signup(keypair, homeserver).await } diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 59f3320..88c4259 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -28,7 +28,7 @@ impl PubkyClient { &self, keypair: &Keypair, homeserver: &PublicKey, - ) -> Result<()> { + ) -> Result { let homeserver = homeserver.to_string(); let Endpoint { mut url, .. } = self.resolve_endpoint(&homeserver).await?; @@ -47,7 +47,9 @@ impl PubkyClient { self.publish_pubky_homeserver(keypair, &homeserver).await?; - Ok(()) + let bytes = response.bytes().await?; + + Ok(Session::deserialize(&bytes)?) } /// Check the current sesison for a given Pubky in its homeserver. @@ -252,7 +254,7 @@ mod tests { .unwrap() .unwrap(); - assert!(session.capabilities.contains(&Capability::root())); + assert!(session.capabilities().contains(&Capability::root())); client.signout(&keypair.public_key()).await.unwrap(); @@ -271,8 +273,8 @@ mod tests { .unwrap() .unwrap(); - assert_eq!(session.pubky, keypair.public_key()); - assert!(session.capabilities.contains(&Capability::root())); + assert_eq!(session.pubky(), &keypair.public_key()); + assert!(session.capabilities().contains(&Capability::root())); } } @@ -306,8 +308,8 @@ mod tests { let session = pubkyauth_response.await.unwrap().unwrap(); - assert_eq!(session.pubky, pubky); - assert_eq!(session.capabilities, capabilities.0); + assert_eq!(session.pubky(), &pubky); + assert_eq!(session.capabilities(), &capabilities.0); // Test access control enforcement diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 6c3cb39..09dc045 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -73,10 +73,16 @@ impl PubkyClient { /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" #[wasm_bindgen] - pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<(), JsValue> { - self.inner_signup(keypair.as_inner(), homeserver.as_inner()) - .await - .map_err(|e| e.into()) + pub async fn signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result { + Ok(Session( + self.inner_signup(keypair.as_inner(), homeserver.as_inner()) + .await + .map_err(|e| JsValue::from(e))?, + )) } /// Check the current sesison for a given Pubky in its homeserver. @@ -129,7 +135,12 @@ impl PubkyClient { let future = async move { this.subscribe_to_auth_response(relay, &client_secret) .await - .map(|_| JsValue::from_str("something")) + .map(|opt| { + opt.map_or_else( + || JsValue::NULL, // Convert `None` to `JsValue::NULL` + |session| JsValue::from(Session(session)), + ) + }) .map_err(|err| JsValue::from_str(&format!("{:?}", err))) }; @@ -143,24 +154,23 @@ impl PubkyClient { Ok(js_tuple) } - // TODO: allow send_auth_token in wasm - // /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the - // /// source of the pubkyauth request url. - // #[wasm_bindgen(js_name = "sendAuthToken")] - // pub async fn send_auth_token( - // &self, - // keypair: &Keypair, - // pubkyauth_url: &str, - // ) -> Result<(), JsValue> { - // let pubkyauth_url: Url = pubkyauth_url - // .try_into() - // .map_err(|_| Error::Generic("Invalid relay Url".into()))?; - // - // self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url) - // .await?; - // - // Ok(()) - // } + /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the + /// source of the pubkyauth request url. + #[wasm_bindgen(js_name = "sendAuthToken")] + pub async fn send_auth_token( + &self, + keypair: &Keypair, + pubkyauth_url: &str, + ) -> Result<(), JsValue> { + let pubkyauth_url: Url = pubkyauth_url + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url) + .await?; + + Ok(()) + } // === Public data === diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index c3005e9..3b27045 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -91,3 +91,9 @@ impl PublicKey { &self.0 } } + +impl From for PublicKey { + fn from(value: pkarr::PublicKey) -> Self { + PublicKey(value) + } +} diff --git a/pubky/src/wasm/session.rs b/pubky/src/wasm/session.rs index ec2e8ca..e838a80 100644 --- a/pubky/src/wasm/session.rs +++ b/pubky/src/wasm/session.rs @@ -2,5 +2,26 @@ use pubky_common::session; use wasm_bindgen::prelude::*; +use super::keys::PublicKey; + #[wasm_bindgen] pub struct Session(pub(crate) session::Session); + +#[wasm_bindgen] +impl Session { + /// Return the [PublicKey] of this session + #[wasm_bindgen] + pub fn pubky(&self) -> PublicKey { + self.0.pubky().clone().into() + } + + /// Return the capabilities that this session has. + #[wasm_bindgen] + pub fn capabilities(&self) -> Vec { + self.0 + .capabilities() + .iter() + .map(|c| c.to_string()) + .collect() + } +}