mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-18 11:36:30 +02:00
add persistent session and users to access tokens
This commit is contained in:
@@ -905,12 +905,16 @@ export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
path: varchar("path"),
|
||||
tokenHash: varchar("tokenHash").notNull(),
|
||||
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||
title: varchar("title"),
|
||||
description: varchar("description"),
|
||||
persistSession: boolean("persistSession").notNull().default(false),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
|
||||
@@ -1125,12 +1125,18 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||
resourceId: integer("resourceId")
|
||||
.notNull()
|
||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
path: text("path"),
|
||||
tokenHash: text("tokenHash").notNull(),
|
||||
sessionLength: integer("sessionLength").notNull(),
|
||||
expiresAt: integer("expiresAt"),
|
||||
title: text("title"),
|
||||
description: text("description"),
|
||||
persistSession: integer("persistSession", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
createdAt: integer("createdAt").notNull()
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
db,
|
||||
Org,
|
||||
orgs,
|
||||
resourceAccessToken,
|
||||
resources,
|
||||
siteResources,
|
||||
sites,
|
||||
@@ -83,6 +84,15 @@ export async function removeUserFromOrg(
|
||||
.delete(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));
|
||||
|
||||
await trx
|
||||
.delete(resourceAccessToken)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceAccessToken.userId, userId),
|
||||
eq(resourceAccessToken.orgId, org.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
await trx.delete(userResources).where(
|
||||
and(
|
||||
eq(userResources.userId, userId),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import {
|
||||
generateId,
|
||||
generateIdFromEntropySize,
|
||||
@@ -8,18 +7,18 @@ import { db } from "@server/db";
|
||||
import {
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
resources
|
||||
resources,
|
||||
userOrgs
|
||||
} from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { createDate, TimeSpan } from "oslo";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -28,7 +27,9 @@ export const generateAccessTokenBodySchema = z.strictObject({
|
||||
validForSeconds: z.int().positive().optional(), // seconds
|
||||
title: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
persistSession: z.boolean().optional().default(false),
|
||||
userId: z.string().optional()
|
||||
});
|
||||
|
||||
export const generateAccssTokenParamsSchema = z.strictObject({
|
||||
@@ -101,7 +102,14 @@ export async function generateAccessToken(
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { validForSeconds, title, path, description } = parsedBody.data;
|
||||
const {
|
||||
validForSeconds,
|
||||
title,
|
||||
path,
|
||||
description,
|
||||
persistSession,
|
||||
userId
|
||||
} = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
@@ -112,6 +120,28 @@ export async function generateAccessToken(
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Resource not found"));
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const [membership] = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, resource.orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!membership) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"User is not a member of this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionLength = validForSeconds
|
||||
? validForSeconds * 1000
|
||||
@@ -133,23 +163,27 @@ export async function generateAccessToken(
|
||||
accessTokenId: id,
|
||||
orgId: resource.orgId,
|
||||
resourceId,
|
||||
userId: userId || null,
|
||||
tokenHash,
|
||||
expiresAt: expiresAt || null,
|
||||
sessionLength: sessionLength,
|
||||
title: title || null,
|
||||
path: path || null,
|
||||
description: description || null,
|
||||
persistSession,
|
||||
createdAt: new Date().getTime()
|
||||
})
|
||||
.returning({
|
||||
accessTokenId: resourceAccessToken.accessTokenId,
|
||||
orgId: resourceAccessToken.orgId,
|
||||
resourceId: resourceAccessToken.resourceId,
|
||||
userId: resourceAccessToken.userId,
|
||||
expiresAt: resourceAccessToken.expiresAt,
|
||||
sessionLength: resourceAccessToken.sessionLength,
|
||||
title: resourceAccessToken.title,
|
||||
path: resourceAccessToken.path,
|
||||
description: resourceAccessToken.description,
|
||||
persistSession: resourceAccessToken.persistSession,
|
||||
createdAt: resourceAccessToken.createdAt
|
||||
})
|
||||
.execute();
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
userResources,
|
||||
roleResources,
|
||||
resourceAccessToken,
|
||||
sites
|
||||
sites,
|
||||
users
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
|
||||
import { sql, eq, or, inArray, and, count, isNull, gt } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
@@ -55,11 +56,16 @@ function queryAccessTokens(
|
||||
accessTokenId: resourceAccessToken.accessTokenId,
|
||||
orgId: resourceAccessToken.orgId,
|
||||
resourceId: resourceAccessToken.resourceId,
|
||||
userId: resourceAccessToken.userId,
|
||||
userName: users.name,
|
||||
username: users.username,
|
||||
userEmail: users.email,
|
||||
sessionLength: resourceAccessToken.sessionLength,
|
||||
expiresAt: resourceAccessToken.expiresAt,
|
||||
tokenHash: resourceAccessToken.tokenHash,
|
||||
title: resourceAccessToken.title,
|
||||
description: resourceAccessToken.description,
|
||||
persistSession: resourceAccessToken.persistSession,
|
||||
createdAt: resourceAccessToken.createdAt,
|
||||
resourceName: resources.name,
|
||||
resourceNiceId: resources.niceId,
|
||||
@@ -75,6 +81,7 @@ function queryAccessTokens(
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
@@ -97,6 +104,7 @@ function queryAccessTokens(
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(sites, eq(resources.resourceId, sites.siteId))
|
||||
.leftJoin(users, eq(resourceAccessToken.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie,
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import {
|
||||
getResourceByDomain,
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
LoginPage,
|
||||
Org,
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
ResourceHeaderAuth,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
ResourcePassword,
|
||||
@@ -21,7 +27,10 @@ import {
|
||||
ResourcePolicyPassword,
|
||||
ResourcePolicyHeaderAuth,
|
||||
ResourceRule,
|
||||
ResourceSession
|
||||
ResourceSession,
|
||||
db,
|
||||
resourceAccessToken,
|
||||
users
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||
@@ -41,11 +50,13 @@ import {
|
||||
enforceResourceSessionLength
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { logRequestAudit } from "./logRequestAudit";
|
||||
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
||||
import { REGIONS } from "@server/db/regions";
|
||||
import { localCache } from "#dynamic/lib/cache";
|
||||
import { APP_VERSION } from "@server/lib/consts";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string(), z.string()).optional(),
|
||||
@@ -350,22 +361,15 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
return await allowAccessToken(
|
||||
res,
|
||||
resource,
|
||||
tokenItem,
|
||||
sessions,
|
||||
dontStripSession,
|
||||
parsedBody.data,
|
||||
ipCC
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,22 +405,15 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
return await allowAccessToken(
|
||||
res,
|
||||
resource,
|
||||
tokenItem,
|
||||
sessions,
|
||||
dontStripSession,
|
||||
parsedBody.data,
|
||||
ipCC
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,22 +663,37 @@ export async function verifyResourceSession(
|
||||
"Resource allowed because access token session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
const [tokenItem] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
eq(
|
||||
resourceAccessToken.accessTokenId,
|
||||
resourceSession.accessTokenId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const userData = tokenItem
|
||||
? await getAccessTokenUserData(
|
||||
tokenItem,
|
||||
resource.orgId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
logAccessTokenRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: null,
|
||||
apiKeyId: resourceSession.accessTokenId
|
||||
}
|
||||
accessTokenId: resourceSession.accessTokenId,
|
||||
tokenTitle: tokenItem?.title ?? null,
|
||||
userData
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
|
||||
if (resourceSession.userSessionId && sso) {
|
||||
@@ -892,6 +904,227 @@ function allowed(
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
async function allowAccessToken(
|
||||
res: Response,
|
||||
resource: Resource,
|
||||
tokenItem: ResourceAccessToken,
|
||||
sessions: Record<string, string> | undefined,
|
||||
dontStripSession: boolean | undefined,
|
||||
auditBody: VerifyResourceSessionSchema,
|
||||
location?: string
|
||||
) {
|
||||
const userData = await getAccessTokenUserData(tokenItem, resource.orgId);
|
||||
|
||||
logAccessTokenRequestAudit(
|
||||
{
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
tokenTitle: tokenItem.title,
|
||||
userData
|
||||
},
|
||||
auditBody
|
||||
);
|
||||
|
||||
if (!tokenItem.persistSession) {
|
||||
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
|
||||
const resourceSessionToken = extractResourceSessionToken(
|
||||
sessions ?? {},
|
||||
resource.ssl
|
||||
);
|
||||
|
||||
if (resourceSessionToken) {
|
||||
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||
let resourceSession: ResourceSession | null | undefined =
|
||||
localCache.get(sessionCacheKey);
|
||||
|
||||
if (!resourceSession) {
|
||||
const result = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
resourceSession = result?.resourceSession;
|
||||
localCache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
|
||||
if (
|
||||
resourceSession &&
|
||||
!resourceSession.isRequestToken &&
|
||||
resourceSession.accessTokenId === tokenItem.accessTokenId
|
||||
) {
|
||||
logger.debug(
|
||||
"Resource allowed because existing access token session is valid"
|
||||
);
|
||||
return allowed(res, userData, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
logAccessTokenAccessAudit(tokenItem, resource, userData, auditBody);
|
||||
return await createAccessTokenSession(res, resource, tokenItem, userData);
|
||||
}
|
||||
|
||||
async function createAccessTokenSession(
|
||||
res: Response,
|
||||
resource: Resource,
|
||||
tokenItem: ResourceAccessToken,
|
||||
userData?: BasicUserData
|
||||
) {
|
||||
const token = generateSessionToken();
|
||||
const sess = 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.session_cookie_name;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain!,
|
||||
token,
|
||||
!resource.ssl,
|
||||
new Date(sess.expiresAt)
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
logger.debug("Access token is valid, creating new session");
|
||||
return allowed(res, userData);
|
||||
}
|
||||
|
||||
async function getAccessTokenUserData(
|
||||
tokenItem: ResourceAccessToken,
|
||||
orgId: string
|
||||
): Promise<BasicUserData | undefined> {
|
||||
if (!tokenItem.userId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cacheKey = `accessTokenUser:${tokenItem.userId}:${orgId}`;
|
||||
const cached = localCache.get(cacheKey) as BasicUserData | null | undefined;
|
||||
if (cached !== undefined) {
|
||||
return cached ?? undefined;
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.userId, tokenItem.userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
localCache.set(cacheKey, null, 5);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||
const userData: BasicUserData = {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||
};
|
||||
|
||||
localCache.set(cacheKey, userData, 12);
|
||||
return userData;
|
||||
}
|
||||
|
||||
function logAccessTokenRequestAudit(
|
||||
data: {
|
||||
resourceId: number;
|
||||
orgId: string;
|
||||
location?: string;
|
||||
accessTokenId: string;
|
||||
tokenTitle: string | null;
|
||||
userData?: BasicUserData;
|
||||
},
|
||||
body: VerifyResourceSessionSchema
|
||||
) {
|
||||
if (data.userData) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: data.resourceId,
|
||||
orgId: data.orgId,
|
||||
location: data.location,
|
||||
user: {
|
||||
username: data.userData.username,
|
||||
userId: data.userData.userId
|
||||
},
|
||||
metadata: {
|
||||
accessTokenId: data.accessTokenId,
|
||||
accessTokenTitle: data.tokenTitle
|
||||
}
|
||||
},
|
||||
body
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: data.resourceId,
|
||||
orgId: data.orgId,
|
||||
location: data.location,
|
||||
apiKey: {
|
||||
name: data.tokenTitle,
|
||||
apiKeyId: data.accessTokenId
|
||||
}
|
||||
},
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
function logAccessTokenAccessAudit(
|
||||
tokenItem: ResourceAccessToken,
|
||||
resource: Resource,
|
||||
userData: BasicUserData | undefined,
|
||||
body: VerifyResourceSessionSchema
|
||||
) {
|
||||
const userAgent =
|
||||
body.headers?.["user-agent"] || body.headers?.["User-Agent"];
|
||||
|
||||
if (userData) {
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
user: {
|
||||
username: userData.username,
|
||||
userId: userData.userId
|
||||
},
|
||||
metadata: {
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
accessTokenTitle: tokenItem.title
|
||||
},
|
||||
userAgent,
|
||||
requestIp: body.requestIp
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
},
|
||||
userAgent,
|
||||
requestIp: body.requestIp
|
||||
});
|
||||
}
|
||||
|
||||
async function headerAuthChallenged(
|
||||
res: Response,
|
||||
redirectPath?: string,
|
||||
|
||||
@@ -809,16 +809,6 @@ authenticated.post(
|
||||
accessToken.generateAccessToken
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/session-token`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceSessionToken),
|
||||
logActionAudit(ActionsEnum.createResourceSessionToken),
|
||||
resource.createResourceSessionToken
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { db } from "@server/db";
|
||||
import { Resource, resources } from "@server/db";
|
||||
import { Resource, resources, users } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -156,11 +156,42 @@ export async function authWithAccessToken(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
let accessAuditUser: { username: string; userId: string } | undefined;
|
||||
if (tokenItem.userId) {
|
||||
const [associatedUser] = await db
|
||||
.select({
|
||||
userId: users.userId,
|
||||
username: users.username
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.userId, tokenItem.userId))
|
||||
.limit(1);
|
||||
if (associatedUser) {
|
||||
accessAuditUser = {
|
||||
userId: associatedUser.userId,
|
||||
username: associatedUser.username
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
apiKey: accessAuditUser
|
||||
? undefined
|
||||
: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
},
|
||||
user: accessAuditUser,
|
||||
metadata: accessAuditUser
|
||||
? {
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
accessTokenTitle: tokenItem.title
|
||||
}
|
||||
: undefined,
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources, users, userOrgs } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { createSession, generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { response } from "@server/lib/response";
|
||||
|
||||
const createResourceSessionTokenParams = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
const createResourceSessionTokenBody = z.strictObject({
|
||||
userId: z.string().nonempty(),
|
||||
idpId: z.coerce.number().int().positive().optional()
|
||||
});
|
||||
|
||||
export type CreateResourceSessionTokenResponse = {
|
||||
requestToken: string;
|
||||
};
|
||||
|
||||
export async function createResourceSessionToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = createResourceSessionTokenParams.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = createResourceSessionTokenBody.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
const { userId, idpId } = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const candidates = await db
|
||||
.select({ userId: users.userId })
|
||||
.from(userOrgs)
|
||||
.innerJoin(users, eq(userOrgs.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(users.userId, userId),
|
||||
eq(userOrgs.orgId, resource.orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`User not found in the organization that owns this resource`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (candidates.length > 1) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Multiple users match this username (external users from different identity providers). Specify idpId to disambiguate."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const targetUserId = candidates[0].userId;
|
||||
|
||||
const appSessionToken = generateSessionToken();
|
||||
const appSession = await createSession(appSessionToken, targetUserId);
|
||||
|
||||
const requestToken = generateSessionToken();
|
||||
await createResourceSession({
|
||||
resourceId,
|
||||
token: requestToken,
|
||||
userSessionId: appSession.sessionId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logger.debug("Resource session token created successfully");
|
||||
|
||||
return response<CreateResourceSessionTokenResponse>(res, {
|
||||
data: { requestToken },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource session token created successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export * from "./getResourceWhitelist";
|
||||
export * from "./authWithWhitelist";
|
||||
export * from "./authWithAccessToken";
|
||||
export * from "./getExchangeToken";
|
||||
export * from "./createResourceSessionToken";
|
||||
export * from "./createResourceRule";
|
||||
export * from "./deleteResourceRule";
|
||||
export * from "./listResourceRules";
|
||||
|
||||
Reference in New Issue
Block a user