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 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 error = ref();
async function authorize() {
const redirect = await $fetch<string>("/api/v1/client/callback", {
const redirect = await $fetch<string>("/api/v1/client/auth/callback", {
method: "POST",
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.
## 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.
@ -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.
## 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.
@ -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.
## 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) {
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);
});
}