mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-24 08:53:04 +10:00
feat: add oidc to admin panel
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M9.41 20H6.5c-1.5 0-2.82-.5-3.89-1.57C1.54 17.38 1 16.09 1 14.58q0-1.95 1.17-3.48a5.25 5.25 0 0 1 3.08-1.95c.42-1.53 1.25-2.77 2.5-3.72C9 4.5 10.42 4 12 4c1.95 0 3.61.68 4.96 2.04C18.32 7.39 19 9.05 19 11c1.15.13 2.11.63 2.86 1.5c.64.73 1 1.56 1.1 2.5H18a5.01 5.01 0 0 0-4-2c-2.8 0-5 2.2-5 5c0 .72.15 1.39.41 2M23 17v2h-2v2h-2v-2h-2.2c-.4 1.2-1.5 2-2.8 2c-1.7 0-3-1.3-3-3s1.3-3 3-3c1.3 0 2.4.8 2.8 2zm-8 1c0-.5-.4-1-1-1s-1 .5-1 1s.4 1 1 1s1-.5 1-1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ nav.label }}
|
{{ nav.label }}
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ active, close }" hydrate-on-visible as="div">
|
<MenuItem v-slot="{ active }" hydrate-on-visible as="div">
|
||||||
<!-- TODO: think this would work better as a NuxtLink instead of a button -->
|
<!-- TODO: think this would work better as a NuxtLink instead of a button -->
|
||||||
<a
|
<a
|
||||||
:class="[
|
:class="[
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="text-sm/6 font-medium text-zinc-100">
|
<div class="text-sm/6 font-medium text-zinc-100">
|
||||||
{{ authMech.name }}
|
{{ authMech.name }}
|
||||||
</div>
|
</div>
|
||||||
<Menu as="div" class="relative ml-auto">
|
<Menu v-if="authMech.route" as="div" class="relative ml-auto">
|
||||||
<MenuButton
|
<MenuButton
|
||||||
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
|
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
|
||||||
>
|
>
|
||||||
@@ -81,10 +81,10 @@
|
|||||||
<div
|
<div
|
||||||
v-for="[key, value] in Object.entries(authMech.settings)"
|
v-for="[key, value] in Object.entries(authMech.settings)"
|
||||||
:key="key"
|
:key="key"
|
||||||
class="flex justify-between gap-x-4 py-2"
|
class="flex flex-nowrap justify-between gap-x-4 py-2"
|
||||||
>
|
>
|
||||||
<dt class="text-zinc-400">{{ key }}</dt>
|
<dt class="text-zinc-400">{{ key }}</dt>
|
||||||
<dd class="text-gray-500">
|
<dd class="text-gray-500 truncate">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IconsSimpleAuthenticationLogo } from "#components";
|
import { IconsSimpleAuthenticationLogo, IconsSSOLogo } from "#components";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||||
import { EllipsisHorizontalIcon } from "@heroicons/vue/20/solid";
|
import { EllipsisHorizontalIcon } from "@heroicons/vue/20/solid";
|
||||||
import { CheckIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
import { CheckIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
@@ -117,9 +117,9 @@ const authenticationMechanisms: Array<{
|
|||||||
name: string;
|
name: string;
|
||||||
mec: AuthMec;
|
mec: AuthMec;
|
||||||
icon: Component;
|
icon: Component;
|
||||||
route: string;
|
route?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
settings?: { [key: string]: string };
|
settings?: { [key: string]: string | undefined } | undefined | boolean;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
name: "Simple (username/password)",
|
name: "Simple (username/password)",
|
||||||
@@ -127,5 +127,15 @@ const authenticationMechanisms: Array<{
|
|||||||
icon: IconsSimpleAuthenticationLogo,
|
icon: IconsSimpleAuthenticationLogo,
|
||||||
route: "/admin/users/auth/simple",
|
route: "/admin/users/auth/simple",
|
||||||
},
|
},
|
||||||
].map((e) => ({ ...e, enabled: enabledMechanisms.includes(e.mec) }));
|
{
|
||||||
|
name: "OpenID Connect",
|
||||||
|
mec: "OpenID" as AuthMec,
|
||||||
|
icon: IconsSSOLogo,
|
||||||
|
},
|
||||||
|
].map((e) => ({
|
||||||
|
...e,
|
||||||
|
enabled: !!enabledMechanisms[e.mec],
|
||||||
|
settings:
|
||||||
|
typeof enabledMechanisms[e.mec] === "object" && enabledMechanisms[e.mec],
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,13 +18,16 @@
|
|||||||
|
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<div>
|
<div>
|
||||||
<AuthSimple v-if="enabledAuths.includes('simple')" />
|
<AuthSimple v-if="enabledAuths.includes(AuthMec.Simple)" />
|
||||||
<div v-if="enabledAuths.length > 1" class="py-4 flex flex-row items-center justify-center gap-x-4 font-bold text-sm text-zinc-600">
|
<div
|
||||||
|
v-if="enabledAuths.length > 1"
|
||||||
|
class="py-4 flex flex-row items-center justify-center gap-x-4 font-bold text-sm text-zinc-600"
|
||||||
|
>
|
||||||
<span class="h-[1px] grow bg-zinc-600" />
|
<span class="h-[1px] grow bg-zinc-600" />
|
||||||
OR
|
OR
|
||||||
<span class="h-[1px] grow bg-zinc-600" />
|
<span class="h-[1px] grow bg-zinc-600" />
|
||||||
</div>
|
</div>
|
||||||
<AuthOpenID v-if="enabledAuths.includes('oidc')" />
|
<AuthOpenID v-if="enabledAuths.includes(AuthMec.OpenID)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
import DropLogo from "~/components/DropLogo.vue";
|
import DropLogo from "~/components/DropLogo.vue";
|
||||||
|
|
||||||
const enabledAuths = await $dropFetch("/api/v1/auth");
|
const enabledAuths = await $dropFetch("/api/v1/auth");
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
model ApplicationSettings {
|
model ApplicationSettings {
|
||||||
timestamp DateTime @id @default(now())
|
timestamp DateTime @id @default(now())
|
||||||
|
|
||||||
enabledAuthencationMechanisms AuthMec[]
|
|
||||||
metadataProviders String[]
|
metadataProviders String[]
|
||||||
|
|
||||||
saveSlotCountLimit Int @default(5)
|
saveSlotCountLimit Int @default(5)
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `enabledAuthencationMechanisms` on the `ApplicationSettings` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ApplicationSettings" DROP COLUMN "enabledAuthencationMechanisms";
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { AuthMec } from "@prisma/client";
|
import { AuthMec } from "@prisma/client";
|
||||||
import aclManager from "~/server/internal/acls";
|
import aclManager from "~/server/internal/acls";
|
||||||
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
const enabledMechanisms: AuthMec[] = await applicationSettings.get(
|
const authData = {
|
||||||
"enabledAuthencationMechanisms",
|
[AuthMec.Simple]: enabledAuthManagers.Simple,
|
||||||
);
|
[AuthMec.OpenID]: enabledAuthManagers.OpenID && enabledAuthManagers.OpenID.generateConfiguration(),
|
||||||
|
}
|
||||||
|
|
||||||
return enabledMechanisms;
|
return authData;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||||
|
|
||||||
export default defineEventHandler((h3) => {
|
export default defineEventHandler(() => {
|
||||||
const authManagers = Object.entries(enabledAuthManagers)
|
const authManagers = Object.entries(enabledAuthManagers)
|
||||||
.filter((e) => !!e[1])
|
.filter((e) => !!e[1])
|
||||||
.map((e) => e[0]);
|
.map((e) => e[0]);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const signinValidator = type({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
if (!enabledAuthManagers.simple)
|
if (!enabledAuthManagers.Simple)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
statusMessage: "Sign in method not enabled",
|
statusMessage: "Sign in method not enabled",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const dbCertificateStore = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
/* empty */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async checkBlacklistCertificate(name: string): Promise<boolean> {
|
async checkBlacklistCertificate(name: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ApplicationSettings } from "@prisma/client";
|
import type { ApplicationSettings } from "@prisma/client";
|
||||||
import { AuthMec } from "@prisma/client";
|
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
|
|
||||||
class ApplicationConfiguration {
|
class ApplicationConfiguration {
|
||||||
@@ -38,7 +37,6 @@ class ApplicationConfiguration {
|
|||||||
async initialiseConfiguration() {
|
async initialiseConfiguration() {
|
||||||
const initialState = await prisma.applicationSettings.create({
|
const initialState = await prisma.applicationSettings.create({
|
||||||
data: {
|
data: {
|
||||||
enabledAuthencationMechanisms: [AuthMec.Simple],
|
|
||||||
metadataProviders: [],
|
metadataProviders: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async push(userId: string, notificationCreateArgs: NotificationCreateArgs) {
|
async push(userId: string, notificationCreateArgs: NotificationCreateArgs) {
|
||||||
|
if (!notificationCreateArgs.nonce)
|
||||||
|
throw new Error("No nonce in notificationCreateArgs");
|
||||||
const notification = await prisma.notification.upsert({
|
const notification = await prisma.notification.upsert({
|
||||||
where: {
|
where: {
|
||||||
nonce: notificationCreateArgs.nonce!!
|
nonce: notificationCreateArgs.nonce,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
import { AuthMec, Prisma } from "@prisma/client";
|
import { AuthMec } from "@prisma/client";
|
||||||
import objectHandler from "../objects";
|
import objectHandler from "../objects";
|
||||||
import { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import * as jdenticon from "jdenticon";
|
import * as jdenticon from "jdenticon";
|
||||||
|
|
||||||
interface OIDCWellKnown {
|
interface OIDCWellKnown {
|
||||||
@@ -39,7 +39,8 @@ export class OIDCManager {
|
|||||||
|
|
||||||
private adminGroup?: string = process.env.OIDC_ADMIN_GROUP;
|
private adminGroup?: string = process.env.OIDC_ADMIN_GROUP;
|
||||||
private usernameClaim: keyof OIDCUserInfo =
|
private usernameClaim: keyof OIDCUserInfo =
|
||||||
(process.env.OIDC_USERNAME_CLAIM as any) ?? "preferred_username";
|
(process.env.OIDC_USERNAME_CLAIM as keyof OIDCUserInfo) ??
|
||||||
|
"preferred_username";
|
||||||
|
|
||||||
private signinStateTable: { [key: string]: OIDCAuthSession } = {};
|
private signinStateTable: { [key: string]: OIDCAuthSession } = {};
|
||||||
|
|
||||||
@@ -121,6 +122,16 @@ export class OIDCManager {
|
|||||||
return new OIDCManager(configuration, clientId, clientSecret, externalUrl);
|
return new OIDCManager(configuration, clientId, clientSecret, externalUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateConfiguration() {
|
||||||
|
return {
|
||||||
|
authorizationUrl: this.oidcConfiguration.authorization_endpoint,
|
||||||
|
scopes: this.oidcConfiguration.scopes_supported.join(", "),
|
||||||
|
adminGroup: this.adminGroup,
|
||||||
|
usernameClaim: this.usernameClaim,
|
||||||
|
externalUrl: this.externalUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
generateAuthSession(): OIDCAuthSession {
|
generateAuthSession(): OIDCAuthSession {
|
||||||
const stateKey = randomUUID();
|
const stateKey = randomUUID();
|
||||||
|
|
||||||
@@ -226,11 +237,12 @@ export class OIDCManager {
|
|||||||
const userId = randomUUID();
|
const userId = randomUUID();
|
||||||
const profilePictureId = randomUUID();
|
const profilePictureId = randomUUID();
|
||||||
|
|
||||||
if (userinfo.picture) {
|
const picture = userinfo.picture;
|
||||||
|
if (picture) {
|
||||||
await objectHandler.createFromSource(
|
await objectHandler.createFromSource(
|
||||||
profilePictureId,
|
profilePictureId,
|
||||||
async () =>
|
async () =>
|
||||||
await $fetch<Readable>(userinfo.picture!!, {
|
await $fetch<Readable>(picture, {
|
||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
@@ -269,6 +281,7 @@ export class OIDCManager {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
credentials: creds as any, // Prisma converts this to the Json type for us
|
credentials: creds as any, // Prisma converts this to the Json type for us
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export default defineNitroPlugin(async (_nitro) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add providers based on their position in the application settings
|
// Add providers based on their position in the application settings
|
||||||
const configuredProviderList =
|
const configuredProviderList =
|
||||||
await applicationSettings.get("metadataProviders");
|
await applicationSettings.get("metadataProviders");
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
import { OIDCManager } from "../internal/oidc";
|
import { OIDCManager } from "../internal/oidc";
|
||||||
|
|
||||||
export const enabledAuthManagers: {
|
export const enabledAuthManagers: {
|
||||||
simple: boolean;
|
[AuthMec.Simple]: boolean;
|
||||||
oidc: OIDCManager | undefined;
|
[AuthMec.OpenID]: OIDCManager | undefined;
|
||||||
} = {
|
} = {
|
||||||
simple: false,
|
[AuthMec.Simple]: false,
|
||||||
oidc: undefined,
|
[AuthMec.OpenID]: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initFunctions: {
|
const initFunctions: {
|
||||||
[K in keyof typeof enabledAuthManagers]: () => Promise<any>;
|
[K in keyof typeof enabledAuthManagers]: () => Promise<unknown>;
|
||||||
} = {
|
} = {
|
||||||
oidc: OIDCManager.prototype.create,
|
[AuthMec.OpenID]: OIDCManager.prototype.create,
|
||||||
simple: async () => {
|
[AuthMec.Simple]: async () => {
|
||||||
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
const disabled = process.env.DISABLE_SIMPLE_AUTH as string | undefined;
|
||||||
return !disabled;
|
return !disabled;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineNitroPlugin(async (nitro) => {
|
export default defineNitroPlugin(async () => {
|
||||||
for (const [key, init] of Object.entries(initFunctions)) {
|
for (const [key, init] of Object.entries(initFunctions)) {
|
||||||
try {
|
try {
|
||||||
const object = await init();
|
const object = await init();
|
||||||
if (!object) break;
|
if (!object) break;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(enabledAuthManagers as any)[key] = object;
|
(enabledAuthManagers as any)[key] = object;
|
||||||
console.log(`enabled auth: ${key}`);
|
console.log(`enabled auth: ${key}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -31,7 +33,7 @@ export default defineNitroPlugin(async (nitro) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
// Add every other auth mechanism here, and fall back to simple if none of them are enabled
|
||||||
if (!enabledAuthManagers.oidc) {
|
if (!enabledAuthManagers[AuthMec.OpenID]) {
|
||||||
enabledAuthManagers.simple = true;
|
enabledAuthManagers[AuthMec.Simple] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import sessionHandler from "~/server/internal/session";
|
|||||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
if (!enabledAuthManagers.oidc) return sendRedirect(h3, "/auth/signin");
|
if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin");
|
||||||
|
|
||||||
const manager = enabledAuthManagers.oidc;
|
const manager = enabledAuthManagers.OpenID;
|
||||||
|
|
||||||
const query = getQuery(h3);
|
const query = getQuery(h3);
|
||||||
const code = query.code?.toString();
|
const code = query.code?.toString();
|
||||||
@@ -29,7 +29,6 @@ export default defineEventHandler(async (h3) => {
|
|||||||
statusMessage: `Failed to sign in: "${user}". Please try again.`,
|
statusMessage: `Failed to sign in: "${user}". Please try again.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await sessionHandler.signin(h3, user.id, true);
|
await sessionHandler.signin(h3, user.id, true);
|
||||||
|
|
||||||
return sendRedirect(h3, "/");
|
return sendRedirect(h3, "/");
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
import { enabledAuthManagers } from "~/server/plugins/04.auth-init";
|
||||||
|
|
||||||
export default defineEventHandler((h3) => {
|
export default defineEventHandler((h3) => {
|
||||||
if (!enabledAuthManagers.oidc) return sendRedirect(h3, "/auth/signin");
|
if (!enabledAuthManagers.OpenID) return sendRedirect(h3, "/auth/signin");
|
||||||
|
|
||||||
const manager = enabledAuthManagers.oidc;
|
const manager = enabledAuthManagers.OpenID;
|
||||||
const { redirectUrl } = manager.generateAuthSession();
|
const { redirectUrl } = manager.generateAuthSession();
|
||||||
|
|
||||||
return sendRedirect(h3, redirectUrl);
|
return sendRedirect(h3, redirectUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user