Merge branch 'develop' of https://github.com/Huskydog9988/drop into more-stuff

This commit is contained in:
Huskydog9988
2025-05-07 18:45:36 -04:00
22 changed files with 560 additions and 204 deletions
+9
View File
@@ -0,0 +1,9 @@
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
export default defineEventHandler((h3) => {
const authManagers = Object.entries(enabledAuthManagers)
.filter((e) => !!e[1])
.map((e) => e[0]);
return authManagers;
});
+7
View File
@@ -7,6 +7,7 @@ import {
checkHashBcrypt,
} from "~/server/internal/security/simple";
import sessionHandler from "~/server/internal/session";
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
const signinValidator = type({
username: "string",
@@ -15,6 +16,12 @@ const signinValidator = type({
});
export default defineEventHandler(async (h3) => {
if (!enabledAuthManagers.simple)
throw createError({
statusCode: 403,
statusMessage: "Sign in method not enabled",
});
const body = signinValidator(await readBody(h3));
if (body instanceof type.errors) {
// hover out.summary to see validation errors
-15
View File
@@ -1,15 +0,0 @@
# Drop P2P System
Drop clients have a variety of P2P or P2P-like methods of data transfer available
## Public (not quite) HTTPS downloads endpoints
These use public HTTPS certificate, and while are authenticated, are 'public' in the sense that they aren't P2P; anyone can connect to them
## Private mTLS P2P endpoints
Drop clients use P2P mTLS aided by the P2P co-ordinator to transfer chunks between themselves. This happens over HTTP.
## Private mTLS Wireguard tunnels
Drop clients can establish P2P Wireguard
+12 -9
View File
@@ -70,14 +70,17 @@ export const dbCertificateStore = () => {
};
},
async blacklistCertificate(name: string) {
await prisma.certificate.update({
where: {
id: name,
},
data: {
blacklisted: true,
},
});
try {
await prisma.certificate.update({
where: {
id: name,
},
data: {
blacklisted: true,
},
});
} finally {
}
},
async checkBlacklistCertificate(name: string): Promise<boolean> {
const result = await prisma.certificate.findUnique({
@@ -88,7 +91,7 @@ export const dbCertificateStore = () => {
blacklisted: true,
},
});
if (result === null) return false;
if (result === null) return true;
return result.blacklisted;
},
};
+281
View File
@@ -0,0 +1,281 @@
import { randomUUID } from "crypto";
import prisma from "../db/database";
import { AuthMec, Prisma } from "@prisma/client";
import objectHandler from "../objects";
import { Readable } from "stream";
import * as jdenticon from "jdenticon";
interface OIDCWellKnown {
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint: string;
scopes_supported: string[];
}
interface OIDCAuthSession {
redirectUrl: string;
callbackUrl: string;
state: string;
}
interface OIDCUserInfo {
sub: string;
name?: string;
preferred_username?: string;
picture?: string;
email?: string;
groups?: Array<string>;
}
export interface OIDCAuthMekCredentialsV1 {
sub: string;
}
export class OIDCManager {
private oidcConfiguration: OIDCWellKnown;
private clientId: string;
private clientSecret: string;
private externalUrl: string;
private adminGroup?: string = process.env.OIDC_ADMIN_GROUP;
private usernameClaim: keyof OIDCUserInfo =
(process.env.OIDC_USERNAME_CLAIM as any) ?? "preferred_username";
private signinStateTable: { [key: string]: OIDCAuthSession } = {};
constructor(
oidcConfiguration: OIDCWellKnown,
clientId: string,
clientSecret: string,
externalUrl: string,
) {
this.oidcConfiguration = oidcConfiguration;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.externalUrl = externalUrl;
}
async create() {
const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined;
let configuration: OIDCWellKnown;
if (wellKnownUrl) {
const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl);
if (
!response.authorization_endpoint ||
!response.scopes_supported ||
!response.token_endpoint ||
!response.userinfo_endpoint
) {
throw new Error("Well known response was invalid");
}
configuration = response;
} else {
const authorizationEndpoint = process.env.OIDC_AUTHORIZATION as
| string
| undefined;
const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined;
const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined;
const scopes = process.env.OIDC_SCOPES as string | undefined;
if (
!authorizationEndpoint ||
!tokenEndpoint ||
!userinfoEndpoint ||
!scopes
) {
const debugObject = {
OIDC_AUTHORIZATION: authorizationEndpoint,
OIDC_TOKEN: tokenEndpoint,
OIDC_USERINFO: userinfoEndpoint,
OIDC_SCOPES: scopes,
};
throw new Error(
"Missing all necessary OIDC configuration: \n" +
Object.entries(debugObject)
.map(([k, v]) => ` ${k}: ${v}`)
.join("\n"),
);
}
configuration = {
authorization_endpoint: authorizationEndpoint,
token_endpoint: tokenEndpoint,
userinfo_endpoint: userinfoEndpoint,
scopes_supported: scopes.split(","),
};
}
if (!configuration)
throw new Error("OIDC try to init without configuration");
const clientId = process.env.OIDC_CLIENT_ID as string | undefined;
const clientSecret = process.env.OIDC_CLIENT_SECRET as string | undefined;
const externalUrl = process.env.EXTERNAL_URL as string | undefined;
if (!clientId || !clientSecret)
throw new Error("Missing client ID or secret for OIDC");
if (!externalUrl) throw new Error("EXTERNAL_URL required for OIDC");
return new OIDCManager(configuration, clientId, clientSecret, externalUrl);
}
generateAuthSession(): OIDCAuthSession {
const stateKey = randomUUID();
const normalisedUrl = new URL(
this.oidcConfiguration.authorization_endpoint,
).toString();
const redirectNormalisedUrl = new URL(this.externalUrl).toString();
const redirectUrl = `${redirectNormalisedUrl}auth/callback/oidc`;
const finalUrl = `${normalisedUrl}?client_id=${this.clientId}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${stateKey}&response_type=code&scope=${encodeURIComponent(this.oidcConfiguration.scopes_supported.join(" "))}`;
const session: OIDCAuthSession = {
redirectUrl: finalUrl,
callbackUrl: redirectUrl,
state: stateKey,
};
this.signinStateTable[stateKey] = session;
return session;
}
async authorize(code: string, state: string) {
const session = this.signinStateTable[state];
if (!session) return "Invalid state parameter";
const tokenEndpoint = new URL(
this.oidcConfiguration.token_endpoint,
).toString();
const userinfoEndpoint = new URL(
this.oidcConfiguration.userinfo_endpoint,
).toString();
const requestBody = new URLSearchParams({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: "authorization_code",
code: code,
redirect_uri: session.callbackUrl,
scope: this.oidcConfiguration.scopes_supported.join(","),
});
try {
const { access_token, token_type } = await $fetch<{
access_token: string;
token_type: string;
id_token: string;
}>(tokenEndpoint, {
body: requestBody,
method: "POST",
});
const userinfo = await $fetch<OIDCUserInfo>(userinfoEndpoint, {
headers: {
Authorization: `${token_type} ${access_token}`,
},
});
const user = await this.fetchOrCreateUser(userinfo);
return user;
} catch (e) {
console.error(e);
return `Request to identity provider failed: ${e}`;
}
}
async fetchOrCreateUser(userinfo: OIDCUserInfo) {
const existingAuthMek = await prisma.linkedAuthMec.findFirst({
where: {
mec: AuthMec.OpenID,
version: 1,
credentials: {
path: ["sub"],
equals: userinfo.sub,
},
},
include: {
user: true,
},
});
if (existingAuthMek) return existingAuthMek.user;
const username = userinfo[this.usernameClaim]?.toString();
if (!username)
return "Invalid username claim in OIDC response: " + this.usernameClaim;
/*
const takenUsername = await prisma.user.count({
where: {
username,
},
});
if (takenUsername > 0)
return "Username already taken. Please contact your server admin.";
*/
const creds: OIDCAuthMekCredentialsV1 = {
sub: userinfo.sub,
};
const userId = randomUUID();
const profilePictureId = randomUUID();
if (userinfo.picture) {
await objectHandler.createFromSource(
profilePictureId,
async () =>
await $fetch<Readable>(userinfo.picture!!, {
responseType: "stream",
}),
{},
[`internal:read`, `${userId}:read`],
);
} else {
await objectHandler.createFromSource(
profilePictureId,
async () => jdenticon.toPng(userinfo.sub, 256),
{},
[`internal:read`, `${userId}:read`],
);
}
const isAdmin =
userinfo.groups !== undefined &&
this.adminGroup !== undefined &&
userinfo.groups.includes(this.adminGroup);
const created = await prisma.linkedAuthMec.create({
data: {
mec: AuthMec.OpenID,
version: 1,
user: {
connectOrCreate: {
where: {
username,
},
create: {
id: userId,
username,
email: userinfo.email ?? "",
displayName: userinfo.name ?? username,
profilePicture: profilePictureId,
admin: isAdmin,
},
},
},
credentials: creds as any, // Prisma converts this to the Json type for us
},
include: {
user: true,
},
});
return created.user;
}
}
+37
View File
@@ -0,0 +1,37 @@
import { OIDCManager } from "../internal/oidc";
export const enabledAuthManagers: {
simple: boolean;
oidc: OIDCManager | undefined;
} = {
simple: false,
oidc: undefined,
};
const initFunctions: {
[K in keyof typeof enabledAuthManagers]: () => Promise<any>;
} = {
oidc: OIDCManager.prototype.create,
simple: async () => {
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
return !disabled;
},
};
export default defineNitroPlugin(async (nitro) => {
for (const [key, init] of Object.entries(initFunctions)) {
try {
const object = await init();
if (!object) break;
(enabledAuthManagers as any)[key] = object;
console.log(`enabled auth: ${key}`);
} catch (e) {
console.warn(e);
}
}
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
if (!enabledAuthManagers.oidc) {
enabledAuthManagers.simple = true;
}
});
+1
View File
@@ -7,6 +7,7 @@ export default defineNitroPlugin((nitro) => {
// Don't handle for API routes
if (event.path.startsWith("/api")) return;
if (event.path.startsWith("/auth")) return;
// Make sure it's a web error
if (!(error instanceof H3Error)) return;
+36
View File
@@ -0,0 +1,36 @@
import sessionHandler from "~/server/internal/session";
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
export default defineEventHandler(async (h3) => {
if (!enabledAuthManagers.oidc) return sendRedirect(h3, "/auth/signin");
const manager = enabledAuthManagers.oidc;
const query = getQuery(h3);
const code = query.code?.toString();
if (!code)
throw createError({
statusCode: 400,
statusMessage: "No code in query params.",
});
const state = query.state?.toString();
if (!state)
throw createError({
statusCode: 400,
statusMessage: "No state in query params.",
});
const user = await manager.authorize(code, state);
if (typeof user === "string")
throw createError({
statusCode: 403,
statusMessage: `Failed to sign in: "${user}". Please try again.`,
});
await sessionHandler.signin(h3, user.id, true);
return sendRedirect(h3, "/");
});
+10
View File
@@ -0,0 +1,10 @@
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
export default defineEventHandler((h3) => {
if (!enabledAuthManagers.oidc) return sendRedirect(h3, "/auth/signin");
const manager = enabledAuthManagers.oidc;
const { redirectUrl } = manager.generateAuthSession();
return sendRedirect(h3, redirectUrl);
});
@@ -1,4 +1,4 @@
import sessionHandler from "../internal/session";
import sessionHandler from "../../internal/session";
export default defineEventHandler(async (h3) => {
await sessionHandler.signout(h3);