mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 08:12:40 +10:00
Code-based authorization for Drop clients (#145)
* feat: code-based authorization * fix: final touches * fix: require session on code fetch endpoint * feat: better error handling * refactor: move auth send to client handler * fix: lint
This commit is contained in:
@ -8,13 +8,19 @@ export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
const clientId = await body.id;
|
||||
|
||||
const data = await clientHandler.fetchClientMetadata(clientId);
|
||||
if (!data)
|
||||
const client = await clientHandler.fetchClient(clientId);
|
||||
if (!client)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid or expired client ID.",
|
||||
});
|
||||
|
||||
if (client.userId != user.userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Not allowed to authorize this client.",
|
||||
});
|
||||
|
||||
const token = await clientHandler.generateAuthToken(clientId);
|
||||
|
||||
return {
|
||||
|
||||
21
server/api/v1/client/auth/code/index.get.ts
Normal file
21
server/api/v1/client/auth/code/index.get.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await sessionHandler.getSession(h3);
|
||||
if (!user) throw createError({ statusCode: 403 });
|
||||
|
||||
const query = getQuery(h3);
|
||||
const code = query.code?.toString()?.toUpperCase();
|
||||
if (!code)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Code required in query params.",
|
||||
});
|
||||
|
||||
const clientId = await clientHandler.fetchClientIdByCode(code);
|
||||
if (!clientId)
|
||||
throw createError({ statusCode: 400, statusMessage: "Invalid code." });
|
||||
|
||||
return clientId;
|
||||
});
|
||||
35
server/api/v1/client/auth/code/index.post.ts
Normal file
35
server/api/v1/client/auth/code/index.post.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const user = await sessionHandler.getSession(h3);
|
||||
if (!user) throw createError({ statusCode: 403 });
|
||||
|
||||
const body = await readBody(h3);
|
||||
const clientId = await body.id;
|
||||
|
||||
const client = await clientHandler.fetchClient(clientId);
|
||||
if (!client)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid or expired client ID.",
|
||||
});
|
||||
|
||||
if (client.userId != user.userId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Not allowed to authorize this client.",
|
||||
});
|
||||
|
||||
if (!client.peer)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "No client listening for authorization.",
|
||||
});
|
||||
|
||||
const token = await clientHandler.generateAuthToken(clientId);
|
||||
|
||||
await clientHandler.sendAuthToken(clientId, token);
|
||||
|
||||
return;
|
||||
});
|
||||
25
server/api/v1/client/auth/code/ws.get.ts
Normal file
25
server/api/v1/client/auth/code/ws.get.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { FetchError } from "ofetch";
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
async open(peer) {
|
||||
try {
|
||||
const h3 = { headers: peer.request?.headers ?? new Headers() };
|
||||
const code = h3.headers.get("Authorization");
|
||||
if (!code)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Code required in Authorization header.",
|
||||
});
|
||||
await clientHandler.connectCodeListener(code, peer);
|
||||
} catch (e) {
|
||||
peer.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
value: (e as FetchError)?.statusMessage,
|
||||
}),
|
||||
);
|
||||
peer.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -13,14 +13,20 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Provide client ID in request params as 'id'",
|
||||
});
|
||||
|
||||
const data = await clientHandler.fetchClientMetadata(providedClientId);
|
||||
if (!data)
|
||||
const client = await clientHandler.fetchClient(providedClientId);
|
||||
if (!client)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Request not found.",
|
||||
});
|
||||
|
||||
if (client.userId && user.userId !== client.userId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Client already claimed.",
|
||||
});
|
||||
|
||||
await clientHandler.attachUserId(providedClientId, user.userId);
|
||||
|
||||
return data;
|
||||
return client.data;
|
||||
});
|
||||
@ -1,3 +1,5 @@
|
||||
import { type } from "arktype";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import type {
|
||||
CapabilityConfiguration,
|
||||
InternalClientCapability,
|
||||
@ -5,23 +7,23 @@ import type {
|
||||
import capabilityManager, {
|
||||
validCapabilities,
|
||||
} from "~/server/internal/clients/capabilities";
|
||||
import clientHandler from "~/server/internal/clients/handler";
|
||||
import clientHandler, { AuthMode } from "~/server/internal/clients/handler";
|
||||
import { parsePlatform } from "~/server/internal/utils/parseplatform";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
const ClientAuthInitiate = type({
|
||||
name: "string",
|
||||
platform: "string",
|
||||
capabilities: "object",
|
||||
mode: type.valueOf(AuthMode).default(AuthMode.Callback),
|
||||
}).configure(throwingArktype);
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readDropValidatedBody(h3, ClientAuthInitiate);
|
||||
|
||||
const name = body.name;
|
||||
const platformRaw = body.platform;
|
||||
const capabilities: Partial<CapabilityConfiguration> =
|
||||
body.capabilities ?? {};
|
||||
|
||||
if (!name || !platformRaw)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Missing name or platform in body",
|
||||
});
|
||||
|
||||
const platform = parsePlatform(platformRaw);
|
||||
if (!platform)
|
||||
throw createError({
|
||||
@ -29,12 +31,6 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Invalid or unsupported platform",
|
||||
});
|
||||
|
||||
if (!capabilities || typeof capabilities !== "object")
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Capabilities must be an array",
|
||||
});
|
||||
|
||||
const capabilityIterable = Object.entries(capabilities) as Array<
|
||||
[InternalClientCapability, object]
|
||||
>;
|
||||
@ -64,11 +60,12 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Invalid capability configuration.",
|
||||
});
|
||||
|
||||
const clientId = await clientHandler.initiate({
|
||||
name,
|
||||
const result = await clientHandler.initiate({
|
||||
name: body.name,
|
||||
platform,
|
||||
capabilities,
|
||||
mode: body.mode,
|
||||
});
|
||||
|
||||
return `/client/${clientId}/callback`;
|
||||
return result;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user