mirror of
https://github.com/aljazceru/pubky-core.git
synced 2025-12-31 04:44:37 +01:00
examples(authz): test thirdPartySignin()
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`
|
||||
* {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -132,5 +132,7 @@ pub async fn signin(
|
||||
|
||||
wtxn.commit()?;
|
||||
|
||||
// TODO: return session to save extra call?
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user