Add base project

Implement auth method
This commit is contained in:
coreyphillips
2024-09-11 22:03:27 -04:00
parent 66649117dc
commit b03d04ccc4
192 changed files with 54913 additions and 3 deletions

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<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>Pubky Auth Demo</title>
<link rel="stylesheet" href="./src/index.css" />
<script type="module">
import "@synonymdev/pubky"
</script>
<script type="module" src="/src/pubky-auth-widget.js"></script>
</head>
<body>
<pubky-auth-widget
relay="https://demo.httprelay.io/link/"
caps="/pub/pubky.app/:rw,/pub/example.com/nested:rw"
>
</pubky-auth-widget>
<main>
<h1>Third Party app!</h1>
<p>this is a demo for using Pubky Auth in an unhosted (no backend) app.</p>
</main>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "pubky-auth-3rd-party",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "npm run dev",
"dev": "vite --host --open",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@synonymdev/pubky": "file:../../../pubky/pkg",
"lit": "^3.2.0",
"qrcode": "^1.5.4"
},
"devDependencies": {
"vite": "^5.4.2"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 1511 1511" width="1511" height="1511"><style>.a{fill:#fff}</style><path d="m269 0h973c148.6 0 269 120.4 269 269v973c0 148.6-120.4 269-269 269h-973c-148.6 0-269-120.4-269-269v-973c0-148.6 120.4-269 269-269z"/><path fill-rule="evenodd" class="a" d="m630.1 1064.3l14.9-59.6c50.5-27.7 90.8-71.7 113.7-124.8-47.3 51.2-115.2 83.3-190.5 83.3-51.9 0-100.1-15.1-140.4-41.2l-39.8 142.3c0 0-199.3 0-200 0l162.4-619.3h210.5l-0.1 0.1q3.7-0.1 7.4-0.1c77.6 0 147.2 34 194.7 88l22-88h201.9l-46.9 180.8 183.7-180.8h248.8l-322.8 332.6 223.9 286.7h-290.8l-116.6-154.6-40.3 154.6c0 0-195 0-195.7 0z"/></svg>

After

Width:  |  Height:  |  Size: 655 B

View File

@@ -0,0 +1,48 @@
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: white;
background: radial-gradient(
circle,
transparent 20%,
#151718 20%,
#151718 80%,
transparent 80%,
transparent
),
radial-gradient(
circle,
transparent 20%,
#151718 20%,
#151718 80%,
transparent 80%,
transparent
)
25px 25px,
linear-gradient(#202020 1px, transparent 2px) 0 -1px,
linear-gradient(90deg, #202020 1px, #151718 2px) -1px 0;
background-size: 50px 50px, 50px 50px, 25px 25px, 25px 25px;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 20rem;
min-height: 100vh;
font-family: var(--font-family);
}
h1 {
font-weight: bold;
font-size: 3.2rem;
line-height: 1.1;
}
main {
max-width: 80rem;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

View File

@@ -0,0 +1,336 @@
import { LitElement, css, html } from 'lit'
import { createRef, ref } from 'lit/directives/ref.js';
import QRCode from 'qrcode'
const DEFAULT_HTTP_RELAY = "https://demo.httprelay.io/link"
/**
*/
export class PubkyAuthWidget extends LitElement {
static get properties() {
return {
/**
* Relay endpoint for the widget to receive Pubky AuthTokens
*
* Internally, a random channel ID will be generated and a
* GET request made for `${realy url}/${channelID}`
*
* If no relay is passed, the widget will use a default relay:
* https://demo.httprelay.io/link
*/
relay: { type: String },
/**
* Capabilities requested or this application encoded as a string.
*/
caps: { type: String },
/**
* Widget's state (open or closed)
*/
open: { type: Boolean },
/**
* Show "copied to clipboard" note
*/
showCopied: { type: Boolean },
}
}
canvasRef = createRef();
constructor() {
if (!window.pubky) {
throw new Error("window.pubky is unavailable, make sure to import `@synonymdev/pubky` before this web component.")
}
super()
this.open = false;
// TODO: allow using mainnet
/** @type {import("@synonymdev/pubky").PubkyClient} */
this.pubkyClient = window.pubky.PubkyClient.testnet();
}
connectedCallback() {
super.connectedCallback()
let [url, promise] = this.pubkyClient.authRequest(this.relay || DEFAULT_HTTP_RELAY, this.caps);
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
}
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>
`
}
_setQr(canvas) {
QRCode.toCanvas(canvas, this.authUrl, {
margin: 2,
scale: 8,
color: {
light: '#fff',
dark: '#000',
},
});
}
_switchOpen() {
this.open = !this.open
}
async _copyToClipboard() {
try {
await navigator.clipboard.writeText(this.authUrl);
this.showCopied = true;
setTimeout(() => { this.showCopied = false }, 1000)
} catch (error) {
console.error('Failed to copy text: ', error);
}
}
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`
* {
box-sizing: border-box;
}
:host {
--full-width: 22rem;
--full-height: 31rem;
--header-height: 3rem;
--closed-width: 3rem;
}
a {
text-decoration: none;
}
button {
padding: 0;
background: none;
border: none;
color: inherit;
cursor: pointer;
}
p {
margin: 0;
}
/** End reset */
#widget {
color: white;
position: fixed;
top: 1rem;
right: 1rem;
background-color:red;
z-index: 99999;
overflow: hidden;
background: rgba(43, 43, 43, .7372549019607844);
border: 1px solid #3c3c3c;
box-shadow: 0 10px 34px -10px rgba(236, 243, 222, .05);
border-radius: 8px;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
width: var(--closed-width);
height: var(--header-height);
will-change: height,width;
transition-property: height, width;
transition-duration: 80ms;
transition-timing-function: ease-in;
}
#widget.open{
width: var(--full-width);
height: var(--full-height);
}
.header {
height: var(--header-height);
display: flex;
justify-content: center;
align-items: center;
}
#widget
.header .text {
display: none;
font-weight: bold;
}
#widget.open
.header .text {
display: block
}
#widget.open
.header {
width: var(--full-width);
justify-content: center;
}
#pubky-icon {
height: 100%;
width: 100%;
}
#widget.open
#pubky-icon {
width: var(--header-height);
height: 74%;
}
#widget-content{
width: var(--full-width);
padding: 0 1rem
}
#widget p {
font-size: .87rem;
line-height: 1rem;
text-align: center;
color: #fff;
opacity: .5;
/* Fix flash wrap in open animation */
text-wrap: nowrap;
}
#qr {
width: 18em !important;
height: 18em !important;
}
.card {
position: relative;
background: #3b3b3b;
border-radius: 5px;
padding: 1rem;
margin-top: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
.card.url {
padding: .625rem;
justify-content: space-between;
max-width:100%;
}
.url p {
display: flex;
align-items: center;
line-height: 1!important;
width: 93%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
.line {
height: 1px;
background-color: #3b3b3b;
flex: 1 1;
margin-bottom: 1rem;
}
.copied {
will-change: opacity;
transition-property: opacity;
transition-duration: 80ms;
transition-timing-function: ease-in;
opacity: 0;
position: absolute;
right: 0;
top: -1.6rem;
font-size: 0.9em;
background: rgb(43 43 43 / 98%);
padding: .5rem;
border-radius: .3rem;
color: #ddd;
}
.copied.show {
opacity:1
}
`
}
}
window.customElements.define('pubky-auth-widget', PubkyAuthWidget)

View File

@@ -0,0 +1,29 @@
# Pubky Auth Example
This example shows 3rd party authorization in Pubky.
It consists of 2 parts:
1. [3rd party app](./3rd-party-app): A web component showing the how to implement a Pubky Auth widget.
2. [Authenticator CLI](./authenticator): A CLI showing the authenticator (key chain) asking user for consent and generating the AuthToken.
## Usage
First you need to be running a local testnet Homeserver, in the root of this repo run
```bash
cargo run --bin pubky_homeserver -- --testnet
```
Run the frontend of the 3rd party app
```bash
cd ./3rd-party-app
npm start
```
Copy the Pubky Auth URL from the frontend.
Finally run the CLI to paste the Pubky Auth in.
You should see the frontend reacting by showing the success of authorization and session details.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
[package]
name = "authenticator"
version = "0.1.0"
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" }
rpassword = "7.3.1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
url = "2.5.2"

View File

@@ -0,0 +1,80 @@
use anyhow::Result;
use clap::Parser;
use pubky::PubkyClient;
use std::path::PathBuf;
use url::Url;
use pubky_common::{capabilities::Capability, crypto::PublicKey};
/// local testnet HOMESERVER
const HOMESERVER: &str = "8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo";
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
/// Path to a recovery_file of the Pubky you want to sign in with
recovery_file: PathBuf,
/// Pubky Auth url
url: Url,
}
#[tokio::main]
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 caps = url
.query_pairs()
.filter_map(|(key, value)| {
if key == "caps" {
return Some(
value
.split(',')
.filter_map(|cap| Capability::try_from(cap).ok())
.collect::<Vec<_>>(),
);
};
None
})
.next()
.unwrap_or_default();
if !caps.is_empty() {
println!("\nRequired Capabilities:");
}
for cap in &caps {
println!(" {} : {:?}", cap.scope, cap.actions);
}
// === Consent form ===
println!("\nEnter your recovery_file's passphrase to confirm:");
let passphrase = rpassword::read_password()?;
let keypair = pubky_common::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?;
println!("Successfully decrypted recovery file...");
println!("PublicKey: {}", keypair.public_key());
let client = PubkyClient::testnet();
// For the purposes of this demo, we need to make sure
// the user has an account on the local homeserver.
if client.signin(&keypair).await.is_err() {
client
.signup(&keypair, &PublicKey::try_from(HOMESERVER).unwrap())
.await?;
};
println!("Sending AuthToken to the 3rd party app...");
client.send_auth_token(&keypair, url).await?;
Ok(())
}