examples(authz): test thirdPartySignin()

This commit is contained in:
nazeh
2024-09-02 17:05:06 +03:00
parent 854fbd8c6f
commit 634d309766
12 changed files with 171 additions and 39 deletions

2
Cargo.lock generated
View File

@@ -140,6 +140,7 @@ name = "authenticator"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"clap",
"pubky",
"pubky-common",
@@ -1511,6 +1512,7 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
name = "pubky"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"bytes",
"js-sys",
"pkarr",

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/pubky.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Lit</title>
<title>Pubky Auth Demo</title>
<link rel="stylesheet" href="./src/index.css" />
<script type="module">
import "@synonymdev/pubky"

View File

@@ -47,6 +47,10 @@ export class PubkyAuthWidget extends LitElement {
this.secret = window.pubky.randomBytes(32)
this.channelId = base64url(window.pubky.hash(this.secret))
// TODO: allow using mainnet
/** @type {import("@synonymdev/pubky").PubkyClient} */
this.pubkyClient = window.pubky.PubkyClient.testnet();
}
connectedCallback() {
@@ -69,7 +73,7 @@ export class PubkyAuthWidget extends LitElement {
fetch(callbackUrl)
.catch(error => console.error("PubkyAuthWidget: Failed to subscribe to http relay channel", error))
.then(this._onCallback)
.then(this._onCallback.bind(this))
}
render() {
@@ -127,9 +131,13 @@ export class PubkyAuthWidget extends LitElement {
const arrayBuffer = await response.arrayBuffer();
// Create a Uint8Array from the ArrayBuffer
const uint8Array = new Uint8Array(arrayBuffer);
const authToken = new Uint8Array(arrayBuffer);
console.log({ uint8Array })
this.pubkyClient.thirdPartySignin(authToken, this.secret)
let session = await this.pubkyClient.session();
console.log({ session })
} catch (error) {
console.error('PubkyAuthWidget: Failed to read incoming AuthToken', error);
}
@@ -145,6 +153,36 @@ export class PubkyAuthWidget extends LitElement {
}
}
render() {
return html`
<div
id="widget"
class=${this.open ? "open" : ""}
>
<button class="header" @click=${this._switchOpen}>
<svg id="pubky-icon" version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1511 1511"><path fill-rule="evenodd" d="m636.3 1066.7 14.9-59.7c50.5-27.7 90.8-71.7 113.7-124.9-47.3 51.3-115.2 83.4-190.6 83.4-51.9 0-100.1-15.1-140.5-41.2L394 1066.7H193.9L356.4 447H567l-.1.1q3.7-.1 7.4-.1c77.7 0 147.3 34 194.8 88l22-88h202.1l-47 180.9L1130 447h249l-323 332.8 224 286.9H989L872.4 912l-40.3 154.7H636.3z" style="fill:#fff"/></svg>
<span class="text">
Pubky Auth
</span>
</button>
<div class="line"></div>
<div id="widget-content">
<p>Scan or copy Pubky auth URL</p>
<div class="card">
<canvas id="qr" ${ref(this._setQr)}></canvas>
</div>
<button class="card url" @click=${this._copyToClipboard}>
<div class="copied ${this.showCopied ? "show" : ""}">Copied to Clipboard</div>
<p>${this.authUrl}</p>
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="10" height="12" rx="2" fill="white"></rect><rect x="3" y="3" width="10" height="12" rx="2" fill="white" stroke="#3B3B3B"></rect></svg>
</button>
</div>
</div>
`
}
static get styles() {
return css`
* {

View File

@@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
base64 = "0.22.1"
clap = { version = "4.5.16", features = ["derive"] }
pubky = { version = "0.1.0", path = "../../../pubky" }
pubky-common = { version = "0.1.0", path = "../../../pubky-common" }

View File

@@ -1,8 +1,9 @@
use anyhow::Result;
use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine};
use clap::Parser;
use pubky::PubkyClient;
use pubky_common::{auth::AuthToken, capabilities::Capability, crypto::PublicKey};
use std::path::PathBuf;
use pubky_common::{capabilities::Capability, crypto::PublicKey};
use std::{collections::HashMap, path::PathBuf};
use url::Url;
/// local testnet HOMESERVER
@@ -22,33 +23,46 @@ struct Cli {
async fn main() -> Result<()> {
let cli = Cli::parse();
let recovery_file = std::fs::read(&cli.recovery_file)?;
println!("\nSuccessfully opened recovery file");
let url = cli.url;
let mut required_capabilities = vec![];
let mut relay = "".to_string();
let client_secret: [u8; 32];
let query_params: HashMap<String, String> = url.query_pairs().into_owned().collect();
for (name, value) in url.query_pairs() {
if name == "relay" {
relay = value.to_string();
}
if name == "secret" {
// client_secret = value.to_string();
}
if name == "capabilities" {
println!("\nRequired Capabilities:");
let relay = query_params
.get("relay")
.map(|r| url::Url::parse(r).expect("Relay query param to be valid URL"))
.expect("Missing relay query param");
for cap_str in value.split(',') {
if let Ok(cap) = Capability::try_from(cap_str) {
println!(" {} : {:?}", cap.resource, cap.abilities);
required_capabilities.push(cap)
};
}
}
let client_secret = query_params
.get("secret")
.map(|s| {
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
let bytes = engine.decode(s).expect("invalid client_secret");
let arr: [u8; 32] = bytes.try_into().expect("invalid client_secret");
arr
})
.expect("Missing client secret");
let required_capabilities = query_params
.get("capabilities")
.map(|caps_string| {
caps_string
.split(',')
.filter_map(|cap| Capability::try_from(cap).ok())
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !required_capabilities.is_empty() {
println!("\nRequired Capabilities:");
}
let recovery_file = std::fs::read(&cli.recovery_file)?;
// println!("\nSuccessfully opened recovery file");
for cap in &required_capabilities {
println!(" {} : {:?}", cap.resource, cap.abilities);
}
// === Consent form ===
@@ -70,7 +84,7 @@ async fn main() -> Result<()> {
};
client
.authorize(&keypair, required_capabilities, [0; 32], &relay)
.authorize(&keypair, required_capabilities, client_secret, &relay)
.await?;
println!("Sending AuthToken to the client...");

View File

@@ -65,7 +65,7 @@ impl AuthToken {
return Err(Error::UnknownVersion);
}
let token: AuthToken = postcard::from_bytes(bytes)?;
let token = AuthToken::deserialize(bytes)?;
match token.version {
0 => {
@@ -102,14 +102,18 @@ impl AuthToken {
postcard::to_allocvec(self).unwrap()
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
Ok(postcard::from_bytes(bytes)?)
}
pub fn pubky(&self) -> &PublicKey {
&self.pubky
}
/// A unique ID for this [AuthToken], which is a concatenation of
/// [AuthToken::subject] and [AuthToken::timestamp].
/// [AuthToken::pubky] and [AuthToken::timestamp].
///
/// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::subject].
/// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::pubky].
fn id(version: u8, bytes: &[u8]) -> Box<[u8]> {
match version {
0 => bytes[65..105].into(),

View File

@@ -132,5 +132,7 @@ pub async fn signin(
wtxn.commit()?;
// TODO: return session to save extra call?
Ok(())
}

View File

@@ -18,6 +18,7 @@ bytes = "^1.7.1"
pubky-common = { version = "0.1.0", path = "../pubky-common" }
pkarr = { workspace = true, features = ["async"] }
base64 = "0.22.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false }

View File

@@ -28,3 +28,7 @@ test('auth', async (t) => {
t.ok(session, "signin")
}
})
// TODO: test end to end
// TODO: test invalid inputs
test.skip("3rd party signin")

View File

@@ -39,6 +39,9 @@ pub enum Error {
#[error(transparent)]
RecoveryFile(#[from] pubky_common::recovery_file::Error),
#[error(transparent)]
AuthToken(#[from] pubky_common::auth::Error),
}
#[cfg(target_arch = "wasm32")]

View File

@@ -1,7 +1,13 @@
use reqwest::{Method, StatusCode};
use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine};
use pkarr::{Keypair, PublicKey};
use pubky_common::{auth::AuthToken, capabilities::Capability, crypto::encrypt, session::Session};
use pubky_common::{
auth::AuthToken,
capabilities::Capability,
crypto::{decrypt, encrypt, hash},
session::Session,
};
use url::Url;
use crate::{error::Result, PubkyClient};
@@ -90,9 +96,13 @@ impl PubkyClient {
url.set_path("/session");
let body = AuthToken::sign(keypair, &homeserver, vec![Capability::root()]).serialize();
let token = AuthToken::sign(keypair, &homeserver, vec![Capability::root()]);
let response = self.request(Method::POST, url).body(body).send().await?;
let response = self
.request(Method::POST, url)
.body(token.serialize())
.send()
.await?;
self.store_session(response);
@@ -104,7 +114,7 @@ impl PubkyClient {
keypair: &Keypair,
capabilities: Vec<Capability>,
client_secret: [u8; 32],
relay: &str,
relay: &Url,
) -> Result<()> {
let pubky = keypair.public_key();
let Endpoint {
@@ -116,9 +126,13 @@ impl PubkyClient {
let encrypted_token = encrypt(&token.serialize(), &client_secret)?;
let mut callback = Url::parse(relay)?;
let channel_id = hash(&client_secret);
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
let channel_id = engine.encode(channel_id.as_bytes());
let mut callback = relay.clone();
let mut path_segments = callback.path_segments_mut().unwrap();
path_segments.push("8Y69yafXgEMafLJKoJ_Ht5zPOVMWuZx_HfKY03U4MTI");
path_segments.push(&channel_id);
drop(path_segments);
@@ -131,6 +145,31 @@ impl PubkyClient {
Ok(())
}
pub async fn inner_third_party_signin(
&self,
encrypted_token: &[u8],
client_secret: &[u8; 32],
) -> Result<()> {
let decrypted = decrypt(encrypted_token, client_secret)?;
let token = AuthToken::deserialize(&decrypted)?;
let pubky = token.pubky();
let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?;
url.set_path("/session");
let response = self
.request(Method::POST, url)
.body(token.serialize())
.send()
.await?;
self.store_session(response);
Ok(())
}
}
#[cfg(test)]

View File

@@ -4,7 +4,7 @@ use std::{
};
use js_sys::{Array, Uint8Array};
use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
use wasm_bindgen::prelude::*;
use crate::PubkyClient;
@@ -95,7 +95,7 @@ impl PubkyClient {
.map_err(|e| e.into())
}
/// Signin to a homeserver.
/// Signin to a homeserver using the root Keypair.
#[wasm_bindgen]
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
self.inner_signin(keypair.as_inner())
@@ -103,6 +103,30 @@ impl PubkyClient {
.map_err(|e| e.into())
}
/// Signin to a homeserver using an AuthToken received from an Authenticator app
#[wasm_bindgen(js_name = "thirdPartySignin")]
pub async fn third_party_signin(
&self,
auth_token: &[u8],
client_secret: js_sys::Uint8Array,
) -> Result<(), JsValue> {
if !js_sys::Uint8Array::instanceof(&client_secret) {
return Err("Expected client_secret to be an instance of Uint8Array".into());
}
let len = client_secret.byte_length();
if len != 32 {
return Err(format!("Expected client_secret to be 32 bytes, got {len}"))?;
}
let mut client_secret_bytes = [0; 32];
client_secret.copy_to(&mut client_secret_bytes);
self.inner_third_party_signin(auth_token, &client_secret_bytes)
.await
.map_err(|e| e.into())
}
// === Public data ===
#[wasm_bindgen]