finalised client APIs and authentication method

This commit is contained in:
DecDuck
2024-10-09 00:37:11 +11:00
parent 425934d3ef
commit d4e2dc8cb6
10 changed files with 112 additions and 5 deletions

View File

@ -134,13 +134,13 @@ import { CheckCircleIcon } from "@heroicons/vue/24/outline";
const route = useRoute(); const route = useRoute();
const clientId = route.params.id; const clientId = route.params.id;
const clientData = await useFetch(`/api/v1/client/callback?id=${clientId}`); const clientData = await useFetch(`/api/v1/client/auth/callback?id=${clientId}`);
const completed = ref(false); const completed = ref(false);
const error = ref(); const error = ref();
async function authorize() { async function authorize() {
const redirect = await $fetch<string>("/api/v1/client/callback", { const redirect = await $fetch<string>("/api/v1/client/auth/callback", {
method: "POST", method: "POST",
body: { id: clientId }, body: { id: clientId },
}); });

View File

@ -0,0 +1,7 @@
import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
export default defineClientEventHandler(async (h3, { fetchUser }) => {
const user = await fetchUser();
return user;
});

View File

@ -3,7 +3,7 @@
Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections. Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections.
## 1. Client requests a handshake ## 1. Client requests a handshake
Client makes request: `POST /api/v1/client/initiate` with information about the client. Client makes request: `POST /api/v1/client/auth/initiate` with information about the client.
Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached. Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached.
@ -13,7 +13,7 @@ Client sends user to the provided URL (in external browser). User signs in using
Server sends redirect to `drop://handshake/[id]/[token]`, where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server. Server sends redirect to `drop://handshake/[id]/[token]`, where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server.
## 3. Client requests certificates ## 3. Client requests certificates
Client makes request: `POST /api/v1/client/handshake` with the token recieved in the previous step. Client makes request: `POST /api/v1/client/auth/handshake` with the token recieved in the previous step.
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all. The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
@ -23,4 +23,4 @@ The server uses it's CA to generate a public-private key pair, the CN of the cli
The client generates a nonce and signs it with their private key. This is then attached to any device-related request. The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
## 4.b Client wants a long-lived session ## 4.b Client wants a long-lived session
The client does the same as above, but instead makes the request to `POST /api/v1/client/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication. The client does the same as above, but instead makes the request to `POST /api/v1/client/auth/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication.

View File

@ -51,4 +51,8 @@ export class CertificateAuthority {
async storeClientCertificate(clientId: string, bundle: CertificateBundle) { async storeClientCertificate(clientId: string, bundle: CertificateBundle) {
await this.certificateStore.store(`client:${clientId}`, bundle); await this.certificateStore.store(`client:${clientId}`, bundle);
} }
async fetchClientCertificate(clientId: string) {
return await this.certificateStore.fetch(`client:${clientId}`);
}
} }

View File

@ -0,0 +1,96 @@
import { Client, User } from "@prisma/client";
import { EventHandlerRequest, H3Event } from "h3";
import droplet from "@drop/droplet";
import { useGlobalCertificateAuthority } from "~/server/plugins/ca";
import prisma from "../db/database";
export type EventHandlerFunction<T> = (
h3: H3Event<EventHandlerRequest>,
utils: ClientUtils
) => Promise<T> | T;
type ClientUtils = {
clientId: string;
fetchClient: () => Promise<Client>;
fetchUser: () => Promise<User>;
};
export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
return defineEventHandler(async (h3) => {
const header = await getHeader(h3, "Authorization");
if (!header) throw createError({ statusCode: 403 });
const [method, ...parts] = header.split(" ");
let clientId: string;
switch (method) {
case "Nonce":
clientId = parts[0];
const nonce = parts[1];
const signature = parts[2];
if (!clientId || !nonce || !signature)
throw createError({ statusCode: 403 });
const ca = useGlobalCertificateAuthority();
const certBundle = await ca.fetchClientCertificate(clientId);
if (!certBundle)
throw createError({
statusCode: 403,
statusMessage: "Invalid client ID",
});
const valid = droplet.verifyNonce(certBundle.cert, nonce, signature);
if (!valid)
throw createError({
statusCode: 403,
statusMessage: "Invalid nonce signature.",
});
break;
default:
throw createError({
statusCode: 403,
});
}
if (clientId === undefined)
throw createError({
statusCode: 500,
statusMessage: "Failed to execute authentication pipeline.",
});
async function fetchClient() {
const client = await prisma.client.findUnique({
where: { id: clientId },
});
if (!client)
throw new Error(
"client util fetch client broke - this should NOT happen"
);
return client;
}
async function fetchUser() {
const client = await prisma.client.findUnique({
where: { id: clientId },
select: {
user: true,
},
});
if (!client)
throw new Error(
"client util fetch client broke - this should NOT happen"
);
return client.user;
}
const utils: ClientUtils = {
clientId,
fetchClient,
fetchUser,
};
return await handler(h3, utils);
});
}