Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pubkey for auth #9

Merged
merged 24 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deploy/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const relay = await run({
default_information: {
name: "Relayed Example",
description: "A lightweight relay written in Deno.",
pubkey: "",
pubkey: Deno.env.get("relayed_pubkey"),
contact: "",
icon: "",
},
Expand Down
124 changes: 101 additions & 23 deletions main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RootResolver } from "./resolvers/root.ts";
import * as gql from "https://esm.sh/graphql@16.8.1";
import { Policy } from "./resolvers/policy.ts";
import { func_ResolvePolicyByKind } from "./resolvers/policy.ts";
import { NostrKind, PublicKey } from "./_libs.ts";
import { NostrKind, PublicKey, verifyEvent } from "./_libs.ts";
import { PolicyStore } from "./resolvers/policy.ts";
import { Policies } from "./resolvers/policy.ts";
import {
Expand Down Expand Up @@ -34,7 +34,7 @@ export type DefaultPolicy = {
export type Relay = {
server: Deno.HttpServer;
url: string;
password: string;
password?: string;
shutdown: () => Promise<void>;
set_policy: (args: {
kind: NostrKind;
Expand All @@ -57,13 +57,6 @@ export async function run(args: {
kv?: Deno.Kv;
}): Promise<Error | Relay> {
const connections = new Map<WebSocket, SubscriptionMap>();
let { password } = args;
if (password == undefined) {
password = Deno.env.get("relayed_pw");
if (!password) {
return new Error("password is not set, please set env var $relayed_pw");
}
}
if (args.kv == undefined) {
args.kv = await Deno.openKv();
}
Expand All @@ -82,6 +75,22 @@ export async function run(args: {
default_information,
);

let { password } = args;
if (!password) {
const { pubkey } = await relayInformationStore.resolveRelayInformation();
if (!pubkey) {
const env_pubkey = Deno.env.get("relayed_pubkey");
if (!env_pubkey) {
password = Deno.env.get("relayed_pw");
if (!password) {
return new Error(
"password or pubkey is not set, please set env var $relayed_pw or $relayed_pubkey",
);
}
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to check the validity of the public key


const eventStore = await EventStore.New(args.kv);

const server = Deno.serve(
Expand Down Expand Up @@ -140,7 +149,7 @@ export type EventReadWriter = {

const root_handler = (
args: {
password: string;
password?: string;
information?: RelayInformation;
connections: Map<WebSocket, SubscriptionMap>;
default_policy: DefaultPolicy;
Expand All @@ -154,6 +163,12 @@ async (req: Request, info: Deno.ServeHandlerInfo) => {
console.log(info.remoteAddr);

const { pathname, protocol } = new URL(req.url);
if (pathname === "/api/auth/login") {
const auth = req.headers.get("authorization");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to validate it

const resp = new Response("ok");
resp.headers.set("set-cookie", `token=${auth}; Path=/; Secure; HttpOnly; SameSite=Strict;`);
return resp;
}
if (pathname == "/api") {
return graphql_handler(args)(req);
}
Expand All @@ -175,27 +190,62 @@ async (req: Request, info: Deno.ServeHandlerInfo) => {

const graphql_handler = (
args: {
password: string;
password?: string;
kv: Deno.Kv;
policyStore: PolicyStore;
relayInformationStore: RelayInformationStore;
},
) =>
async (req: Request) => {
const { password, policyStore } = args;
if (req.method == "POST") {
const query = await req.json();
const pw = req.headers.get("password");
if (pw != password) {
return new Response(`{"errors":"incorrect password"}`);
try {
const query = await req.json();
if (!args.password) {
const cookie = req.headers.get("cookie");
const token = cookie?.split(";").find((c) => c.includes("token"))?.split("=")[1].split(
" ",
)[1];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is not readable at all.
See https://deno.land/std@0.148.0/http/mod.ts?s=getCookies

const event = token ? JSON.parse(atob(token)) : undefined;
if (event) {
if (!await verifyEvent(event)) {
return new Response(`{"errors":"token not verified"}`);
}
const { pubkey } = await args.relayInformationStore.resolveRelayInformation();
if (!pubkey) {
return new Response(`{"errors":"relay pubkey not set"}`);
}
const relayPubkey = PublicKey.FromString(pubkey);
if (relayPubkey instanceof Error) {
return new Response(`{"errors":"relay pubkey not valid"}`);
}
if (event.pubkey != relayPubkey.hex) {
return new Response(`{"errors":"you are not admin"}`);
}
const result = await gql.graphql({
schema: schema,
source: query.query,
variableValues: query.variables,
rootValue: RootResolver(args),
});
return new Response(JSON.stringify(result));
}
return new Response(`{"errors":"please login first"}`);
} else {
const password = req.headers.get("password");
if (password != args.password) {
return new Response(`{"errors":"password not correct"}`);
}
const result = await gql.graphql({
schema: schema,
source: query.query,
variableValues: query.variables,
rootValue: RootResolver(args),
});
return new Response(JSON.stringify(result));
}
} catch (error) {
return new Response(`{"errors":"${error}"}`);
}
const result = await gql.graphql({
schema: schema,
source: query.query,
variableValues: query.variables,
rootValue: RootResolver(args),
});
return new Response(JSON.stringify(result));
} else if (req.method == "GET") {
const res = new Response(graphiql);
res.headers.set("content-type", "html");
Expand Down Expand Up @@ -293,6 +343,7 @@ const graphiql = `
</head>

<body>
<button id="nip7">Login with NIP-07 extensions</button>
<div id="graphiql">Loading...</div>
<script>
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
Expand All @@ -307,6 +358,33 @@ const graphiql = `
plugins: [explorerPlugin],
}),
);
const nip7 = document.getElementById('nip7');
nip7.onclick = async () => {
if ("nostr" in window) {
try {
const ext = window.nostr;
const pubkey = await ext.getPublicKey();
const unsigned_event = {
pubkey,
content: "",
created_at: Math.floor(Date.now() / 1000),
kind: 27235,
tags: [],
}
const event = await ext.signEvent(unsigned_event);
fetch('/api/auth/login', {
headers: {
authorization: "Nostr " + btoa(JSON.stringify(event)),
},
credentials: 'include'
})
} catch (e) {
console.error(e);
}
} else {
alert("Nostr extension not found");
}
};
</script>
</body>
</html>`;
1 change: 1 addition & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ async function randomEvent(ctx: InMemoryAccountContext, kind?: NostrKind, conten

async function queryGql(relay: Relay, query: string, variables?: object) {
const { hostname, port } = new URL(relay.url);
if (!relay.password) throw new Error("relay.password is not set on test");
const res = await fetch(`http://${hostname}:${port}/api`, {
method: "POST",
headers: {
Expand Down
Loading