diff --git a/server/auth/canUserAccessResource.ts b/server/auth/canUserAccessResource.ts new file mode 100644 index 00000000..bdafaa0d --- /dev/null +++ b/server/auth/canUserAccessResource.ts @@ -0,0 +1,45 @@ +import db from "@server/db"; +import { and, eq } from "drizzle-orm"; +import { roleResources, userResources } from "@server/db/schema"; + +export async function canUserAccessResource({ + userId, + resourceId, + roleId +}: { + userId: string; + resourceId: number; + roleId: number; +}): Promise { + const roleResourceAccess = await db + .select() + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + eq(roleResources.roleId, roleId) + ) + ) + .limit(1); + + if (roleResourceAccess.length > 0) { + return true; + } + + const userResourceAccess = await db + .select() + .from(userResources) + .where( + and( + eq(userResources.userId, userId), + eq(userResources.resourceId, resourceId) + ) + ) + .limit(1); + + if (userResourceAccess.length > 0) { + return true; + } + + return false; +} diff --git a/server/auth/verifyResourceAccessToken.ts b/server/auth/verifyResourceAccessToken.ts new file mode 100644 index 00000000..ce74952b --- /dev/null +++ b/server/auth/verifyResourceAccessToken.ts @@ -0,0 +1,67 @@ +import db from "@server/db"; +import { + Resource, + ResourceAccessToken, + resourceAccessToken, +} from "@server/db/schema"; +import { and, eq } from "drizzle-orm"; +import { isWithinExpirationDate } from "oslo"; +import { verifyPassword } from "./password"; + +export async function verifyResourceAccessToken({ + resource, + accessTokenId, + accessToken +}: { + resource: Resource; + accessTokenId: string; + accessToken: string; +}): Promise<{ + valid: boolean; + error?: string; + tokenItem?: ResourceAccessToken; +}> { + const [result] = await db + .select() + .from(resourceAccessToken) + .where( + and( + eq(resourceAccessToken.resourceId, resource.resourceId), + eq(resourceAccessToken.accessTokenId, accessTokenId) + ) + ) + .limit(1); + + const tokenItem = result; + + if (!tokenItem) { + return { + valid: false, + error: "Access token does not exist for resource" + }; + } + + const validCode = await verifyPassword(accessToken, tokenItem.tokenHash); + + if (!validCode) { + return { + valid: false, + error: "Invalid access token" + }; + } + + if ( + tokenItem.expiresAt && + !isWithinExpirationDate(new Date(tokenItem.expiresAt)) + ) { + return { + valid: false, + error: "Access token has expired" + }; + } + + return { + valid: true, + tokenItem + }; +} diff --git a/server/lib/config.ts b/server/lib/config.ts index 3da4ea36..203a6441 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -11,9 +11,9 @@ const portSchema = z.number().positive().gt(0).lte(65535); const hostnameSchema = z .string() .regex( - /^(?!-)[a-zA-Z0-9-]{1,63}(?(res, data); } +async function createAccessTokenSession( + res: Response, + resource: Resource, + tokenItem: ResourceAccessToken +) { + const token = generateSessionToken(); + await createResourceSession({ + resourceId: resource.resourceId, + token, + accessTokenId: tokenItem.accessTokenId, + sessionLength: tokenItem.sessionLength, + expiresAt: tokenItem.expiresAt, + doNotExtend: tokenItem.expiresAt ? true : false + }); + const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`; + const cookie = serializeResourceSessionCookie(cookieName, token); + res.appendHeader("Set-Cookie", cookie); + logger.debug("Access token is valid, creating new session") + return response(res, { + data: { valid: true }, + success: true, + error: false, + message: "Access allowed", + status: HttpCode.OK + }); +} + async function isUserAllowedToAccessResource( user: User, resource: Resource ): Promise { - if (config.getRawConfig().flags?.require_email_verification && !user.emailVerified) { + if ( + config.getRawConfig().flags?.require_email_verification && + !user.emailVerified + ) { return false; } diff --git a/server/routers/resource/authWithAccessToken.ts b/server/routers/resource/authWithAccessToken.ts index fdc4b254..a4340f77 100644 --- a/server/routers/resource/authWithAccessToken.ts +++ b/server/routers/resource/authWithAccessToken.ts @@ -14,9 +14,7 @@ import { } from "@server/auth/sessions/resource"; import config from "@server/lib/config"; import logger from "@server/logger"; -import { verify } from "@node-rs/argon2"; -import { isWithinExpirationDate } from "oslo"; -import { verifyPassword } from "@server/auth/password"; +import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; const authWithAccessTokenBodySchema = z .object({ @@ -69,58 +67,38 @@ export async function authWithAccessToken( const { accessToken, accessTokenId } = parsedBody.data; try { - const [result] = await db + const [resource] = await db .select() - .from(resourceAccessToken) - .where( - and( - eq(resourceAccessToken.resourceId, resourceId), - eq(resourceAccessToken.accessTokenId, accessTokenId) - ) - ) - .leftJoin( - resources, - eq(resources.resourceId, resourceAccessToken.resourceId) - ) + .from(resources) + .where(eq(resources.resourceId, resourceId)) .limit(1); - const resource = result?.resources; - const tokenItem = result?.resourceAccessToken; - - if (!tokenItem) { - return next( - createHttpError( - HttpCode.UNAUTHORIZED, - createHttpError( - HttpCode.BAD_REQUEST, - "Access token does not exist for resource" - ) - ) - ); - } - if (!resource) { return next( - createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist") + createHttpError(HttpCode.NOT_FOUND, "Resource not found") ); } - const validCode = await verifyPassword(accessToken, tokenItem.tokenHash); + const { valid, error, tokenItem } = await verifyResourceAccessToken({ + resource, + accessTokenId, + accessToken + }); - if (!validCode) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "Invalid access token") - ); - } - - if ( - tokenItem.expiresAt && - !isWithinExpirationDate(new Date(tokenItem.expiresAt)) - ) { + if (!valid) { return next( createHttpError( HttpCode.UNAUTHORIZED, - "Access token has expired" + error || "Invalid access token" + ) + ); + } + + if (!tokenItem || !resource) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "Access token does not exist for resource" ) ); } diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 708fa413..829a3a93 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -56,6 +56,7 @@ export async function traefikConfigProvider( config.getRawConfig().server.resource_session_cookie_name, userSessionCookieName: config.getRawConfig().server.session_cookie_name, + accessTokenQueryParam: "p_token" }, }, }, diff --git a/server/setup/setupServerAdmin.ts b/server/setup/setupServerAdmin.ts index 54d38338..70faff86 100644 --- a/server/setup/setupServerAdmin.ts +++ b/server/setup/setupServerAdmin.ts @@ -69,6 +69,8 @@ export async function setupServerAdmin() { const userId = generateId(15); + await trx.update(users).set({ serverAdmin: false }); + await db.insert(users).values({ userId: userId, email: email, diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 2cf37848..49041a0d 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -45,11 +45,10 @@ export default async function ResourceAuthPage(props: { const user = await getUser({ skipCheckVerifyEmail: true }); if (!authInfo) { - { - /* @ts-ignore */ - } // TODO: fix this + // TODO: fix this return (
+ {/* @ts-ignore */}
);