refactor auth to work cross domain and with http resources closes #100
This commit is contained in:
parent
6050a0a7d7
commit
9f1f2910e4
27 changed files with 688 additions and 201 deletions
|
@ -10,9 +10,9 @@ server:
|
|||
next_port: 3002
|
||||
internal_hostname: "pangolin"
|
||||
secure_cookies: true
|
||||
session_cookie_name: "p_session"
|
||||
resource_session_cookie_name: "p_resource_session"
|
||||
session_cookie_name: "p_session_token"
|
||||
resource_access_token_param: "p_token"
|
||||
resource_session_request_param: "p_session_request"
|
||||
|
||||
traefik:
|
||||
cert_resolver: "letsencrypt"
|
||||
|
|
|
@ -10,9 +10,9 @@ server:
|
|||
next_port: 3002
|
||||
internal_hostname: "pangolin"
|
||||
secure_cookies: true
|
||||
session_cookie_name: "p_session"
|
||||
resource_session_cookie_name: "p_resource_session"
|
||||
session_cookie_name: "p_session_token"
|
||||
resource_access_token_param: "p_token"
|
||||
resource_session_request_param: "p_session_request"
|
||||
cors:
|
||||
origins: ["https://{{.DashboardDomain}}"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
"moment": "2.30.1",
|
||||
"next": "15.1.3",
|
||||
"next-themes": "0.4.4",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.16",
|
||||
"oslo": "1.2.1",
|
||||
|
|
|
@ -3,7 +3,13 @@ import {
|
|||
encodeHexLowerCase
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Session, sessions, User, users } from "@server/db/schema";
|
||||
import {
|
||||
resourceSessions,
|
||||
Session,
|
||||
sessions,
|
||||
User,
|
||||
users
|
||||
} from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
@ -13,9 +19,14 @@ import logger from "@server/logger";
|
|||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
export const SESSION_COOKIE_EXPIRES =
|
||||
1000 *
|
||||
60 *
|
||||
60 *
|
||||
config.getRawConfig().server.dashboard_session_length_hours;
|
||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||
export const COOKIE_DOMAIN =
|
||||
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
||||
|
||||
export function generateSessionToken(): string {
|
||||
const bytes = new Uint8Array(20);
|
||||
|
@ -65,12 +76,21 @@ export async function validateSessionToken(
|
|||
session.expiresAt = new Date(
|
||||
Date.now() + SESSION_COOKIE_EXPIRES
|
||||
).getTime();
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(sessions.sessionId, session.sessionId));
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.update(sessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(sessions.sessionId, session.sessionId));
|
||||
|
||||
await trx
|
||||
.update(resourceSessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(resourceSessions.userSessionId, session.sessionId));
|
||||
});
|
||||
}
|
||||
return { session, user };
|
||||
}
|
||||
|
@ -90,9 +110,9 @@ export function serializeSessionCookie(
|
|||
if (isSecure) {
|
||||
logger.debug("Setting cookie for secure origin");
|
||||
if (SECURE_COOKIES) {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
}
|
||||
} else {
|
||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
||||
|
|
|
@ -6,19 +6,20 @@ import { eq, and } from "drizzle-orm";
|
|||
import config from "@server/lib/config";
|
||||
|
||||
export const SESSION_COOKIE_NAME =
|
||||
config.getRawConfig().server.resource_session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
config.getRawConfig().server.session_cookie_name;
|
||||
export const SESSION_COOKIE_EXPIRES =
|
||||
1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours;
|
||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
||||
|
||||
export async function createResourceSession(opts: {
|
||||
token: string;
|
||||
resourceId: number;
|
||||
passwordId?: number;
|
||||
pincodeId?: number;
|
||||
whitelistId?: number;
|
||||
accessTokenId?: string;
|
||||
usedOtp?: boolean;
|
||||
isRequestToken?: boolean;
|
||||
passwordId?: number | null;
|
||||
pincodeId?: number | null;
|
||||
userSessionId?: string | null;
|
||||
whitelistId?: number | null;
|
||||
accessTokenId?: string | null;
|
||||
doNotExtend?: boolean;
|
||||
expiresAt?: number | null;
|
||||
sessionLength?: number | null;
|
||||
|
@ -27,7 +28,8 @@ export async function createResourceSession(opts: {
|
|||
!opts.passwordId &&
|
||||
!opts.pincodeId &&
|
||||
!opts.whitelistId &&
|
||||
!opts.accessTokenId
|
||||
!opts.accessTokenId &&
|
||||
!opts.userSessionId
|
||||
) {
|
||||
throw new Error("Auth method must be provided");
|
||||
}
|
||||
|
@ -47,7 +49,9 @@ export async function createResourceSession(opts: {
|
|||
pincodeId: opts.pincodeId || null,
|
||||
whitelistId: opts.whitelistId || null,
|
||||
doNotExtend: opts.doNotExtend || false,
|
||||
accessTokenId: opts.accessTokenId || null
|
||||
accessTokenId: opts.accessTokenId || null,
|
||||
isRequestToken: opts.isRequestToken || false,
|
||||
userSessionId: opts.userSessionId || null
|
||||
};
|
||||
|
||||
await db.insert(resourceSessions).values(session);
|
||||
|
@ -162,22 +166,25 @@ export async function invalidateAllSessions(
|
|||
|
||||
export function serializeResourceSessionCookie(
|
||||
cookieName: string,
|
||||
token: string
|
||||
domain: string,
|
||||
token: string,
|
||||
isHttp: boolean = false
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
if (SECURE_COOKIES && !isHttp) {
|
||||
return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
|
||||
} else {
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlankResourceSessionTokenCookie(
|
||||
cookieName: string
|
||||
cookieName: string,
|
||||
domain: string
|
||||
): string {
|
||||
if (SECURE_COOKIES) {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
|
||||
} else {
|
||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -313,6 +313,10 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||
doNotExtend: integer("doNotExtend", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isRequestToken: integer("isRequestToken", { mode: "boolean" }),
|
||||
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
passwordId: integer("passwordId").references(
|
||||
() => resourcePassword.passwordId,
|
||||
{
|
||||
|
|
|
@ -2,7 +2,7 @@ import { runSetupFunctions } from "./setup";
|
|||
import { createApiServer } from "./apiServer";
|
||||
import { createNextServer } from "./nextServer";
|
||||
import { createInternalServer } from "./internalServer";
|
||||
import { User, UserOrg } from "./db/schema";
|
||||
import { Session, User, UserOrg } from "./db/schema";
|
||||
|
||||
async function startServers() {
|
||||
await runSetupFunctions();
|
||||
|
@ -24,6 +24,7 @@ declare global {
|
|||
namespace Express {
|
||||
interface Request {
|
||||
user?: User;
|
||||
session?: Session;
|
||||
userOrg?: UserOrg;
|
||||
userOrgRoleId?: number;
|
||||
userOrgId?: string;
|
||||
|
|
|
@ -61,8 +61,20 @@ const configSchema = z.object({
|
|||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||
secure_cookies: z.boolean(),
|
||||
session_cookie_name: z.string(),
|
||||
resource_session_cookie_name: z.string(),
|
||||
resource_access_token_param: z.string(),
|
||||
resource_session_request_param: z.string(),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
resource_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
.gt(0)
|
||||
.optional()
|
||||
.default(720),
|
||||
cors: z
|
||||
.object({
|
||||
origins: z.array(z.string()).optional(),
|
||||
|
@ -241,8 +253,6 @@ export class Config {
|
|||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.session_cookie_name;
|
||||
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.resource_session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||
?.disable_signup_without_invite
|
||||
|
@ -254,6 +264,8 @@ export class Config {
|
|||
: "false";
|
||||
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
||||
parsedConfig.data.server.resource_access_token_param;
|
||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||
parsedConfig.data.server.resource_session_request_param;
|
||||
|
||||
this.rawConfig = parsedConfig.data;
|
||||
}
|
||||
|
|
|
@ -5,18 +5,17 @@ import response from "@server/lib/response";
|
|||
import logger from "@server/logger";
|
||||
import {
|
||||
createBlankSessionTokenCookie,
|
||||
invalidateSession,
|
||||
SESSION_COOKIE_NAME
|
||||
invalidateSession
|
||||
} from "@server/auth/sessions/app";
|
||||
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||
|
||||
export async function logout(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
const sessionId = req.cookies[SESSION_COOKIE_NAME];
|
||||
|
||||
if (!sessionId) {
|
||||
const { user, session } = await verifySession(req);
|
||||
if (!user || !session) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
|
@ -26,7 +25,7 @@ export async function logout(
|
|||
}
|
||||
|
||||
try {
|
||||
await invalidateSession(sessionId);
|
||||
await invalidateSession(session.sessionId);
|
||||
const isSecure = req.protocol === "https";
|
||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
||||
|
||||
|
|
175
server/routers/badger/exchangeSession.ts
Normal file
175
server/routers/badger/exchangeSession.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import HttpCode from "@server/types/HttpCode";
|
||||
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 { resourceAccessToken, resources, sessions } from "@server/db/schema";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie,
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { generateSessionToken } from "@server/auth";
|
||||
import { SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
|
||||
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
||||
import config from "@server/lib/config";
|
||||
import { response } from "@server/lib";
|
||||
|
||||
const exchangeSessionBodySchema = z.object({
|
||||
requestToken: z.string(),
|
||||
host: z.string()
|
||||
});
|
||||
|
||||
export type ExchangeSessionBodySchema = z.infer<
|
||||
typeof exchangeSessionBodySchema
|
||||
>;
|
||||
|
||||
export type ExchangeSessionResponse = {
|
||||
valid: boolean;
|
||||
cookie?: string;
|
||||
};
|
||||
|
||||
export async function exchangeSession(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
logger.debug("Exchange session: Badger sent", req.body);
|
||||
|
||||
const parsedBody = exchangeSessionBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { requestToken, host } = parsedBody.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.fullDomain, host))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with host ${host} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceSession: requestSession } =
|
||||
await validateResourceSessionToken(
|
||||
requestToken,
|
||||
resource.resourceId
|
||||
);
|
||||
|
||||
if (!requestSession) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
|
||||
);
|
||||
}
|
||||
|
||||
if (!requestSession.isRequestToken) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid request token")
|
||||
);
|
||||
}
|
||||
|
||||
await db.delete(sessions).where(eq(sessions.sessionId, requestToken));
|
||||
|
||||
const token = generateSessionToken();
|
||||
|
||||
if (requestSession.userSessionId) {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.sessionId, requestSession.userSessionId))
|
||||
.limit(1);
|
||||
if (res) {
|
||||
await createResourceSession({
|
||||
token,
|
||||
resourceId: resource.resourceId,
|
||||
isRequestToken: false,
|
||||
userSessionId: requestSession.userSessionId,
|
||||
doNotExtend: false,
|
||||
expiresAt: res.expiresAt,
|
||||
sessionLength: SESSION_COOKIE_EXPIRES
|
||||
});
|
||||
}
|
||||
} else if (requestSession.accessTokenId) {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
eq(
|
||||
resourceAccessToken.accessTokenId,
|
||||
requestSession.accessTokenId
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (res) {
|
||||
await createResourceSession({
|
||||
token,
|
||||
resourceId: resource.resourceId,
|
||||
isRequestToken: false,
|
||||
accessTokenId: requestSession.accessTokenId,
|
||||
doNotExtend: true,
|
||||
expiresAt: res.expiresAt,
|
||||
sessionLength: res.sessionLength
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await createResourceSession({
|
||||
token,
|
||||
resourceId: resource.resourceId,
|
||||
isRequestToken: false,
|
||||
passwordId: requestSession.passwordId,
|
||||
pincodeId: requestSession.pincodeId,
|
||||
userSessionId: requestSession.userSessionId,
|
||||
whitelistId: requestSession.whitelistId,
|
||||
accessTokenId: requestSession.accessTokenId,
|
||||
doNotExtend: false,
|
||||
expiresAt: new Date(
|
||||
Date.now() + SESSION_COOKIE_EXPIRES
|
||||
).getTime(),
|
||||
sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES
|
||||
});
|
||||
}
|
||||
|
||||
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain,
|
||||
token,
|
||||
!resource.ssl
|
||||
);
|
||||
|
||||
logger.debug(JSON.stringify("Exchange cookie: " + cookie));
|
||||
return response<ExchangeSessionResponse>(res, {
|
||||
data: { valid: true, cookie },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Session exchanged successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to exchange session"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from "./verifySession";
|
||||
export * from "./exchangeSession";
|
||||
|
|
|
@ -4,17 +4,17 @@ import createHttpError from "http-errors";
|
|||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { response } from "@server/lib/response";
|
||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
ResourceAccessToken,
|
||||
resourceAccessToken,
|
||||
ResourcePassword,
|
||||
resourcePassword,
|
||||
ResourcePincode,
|
||||
resourcePincode,
|
||||
resources,
|
||||
resourceWhitelist,
|
||||
User,
|
||||
userOrgs
|
||||
sessions,
|
||||
userOrgs,
|
||||
users
|
||||
} from "@server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
@ -27,6 +27,12 @@ import { Resource, roleResources, userResources } from "@server/db/schema";
|
|||
import logger from "@server/logger";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import { generateSessionToken } from "@server/auth";
|
||||
import NodeCache from "node-cache";
|
||||
|
||||
// We'll see if this speeds anything up
|
||||
const cache = new NodeCache({
|
||||
stdTTL: 5 // seconds
|
||||
});
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string()).optional(),
|
||||
|
@ -53,7 +59,7 @@ export async function verifyResourceSession(
|
|||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
logger.debug("Badger sent", req.body); // remove when done testing
|
||||
logger.debug("Verify session: Badger sent", req.body); // remove when done testing
|
||||
|
||||
const parsedBody = verifyResourceSessionSchema.safeParse(req.body);
|
||||
|
||||
|
@ -67,26 +73,52 @@ export async function verifyResourceSession(
|
|||
}
|
||||
|
||||
try {
|
||||
const { sessions, host, originalRequestURL, accessToken: token } =
|
||||
parsedBody.data;
|
||||
const {
|
||||
sessions,
|
||||
host,
|
||||
originalRequestURL,
|
||||
accessToken: token
|
||||
} = parsedBody.data;
|
||||
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, host))
|
||||
.limit(1);
|
||||
const resourceCacheKey = `resource:${host}`;
|
||||
let resourceData:
|
||||
| {
|
||||
resource: Resource | null;
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
}
|
||||
| undefined = cache.get(resourceCacheKey);
|
||||
|
||||
const resource = result?.resources;
|
||||
const pincode = result?.resourcePincode;
|
||||
const password = result?.resourcePassword;
|
||||
if (!resourceData) {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
resourcePincode,
|
||||
eq(resourcePincode.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourcePassword,
|
||||
eq(resourcePassword.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, host))
|
||||
.limit(1);
|
||||
|
||||
if (!result) {
|
||||
logger.debug("Resource not found", host);
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
resourceData = {
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword
|
||||
};
|
||||
|
||||
cache.set(resourceCacheKey, resourceData);
|
||||
}
|
||||
|
||||
const { resource, pincode, password } = resourceData;
|
||||
|
||||
if (!resource) {
|
||||
logger.debug("Resource not found", host);
|
||||
|
@ -145,37 +177,31 @@ export async function verifyResourceSession(
|
|||
return notAllowed(res);
|
||||
}
|
||||
|
||||
const sessionToken =
|
||||
sessions[config.getRawConfig().server.session_cookie_name];
|
||||
|
||||
// check for unified login
|
||||
if (sso && sessionToken) {
|
||||
const { session, user } = await validateSessionToken(sessionToken);
|
||||
if (session && user) {
|
||||
const isAllowed = await isUserAllowedToAccessResource(
|
||||
user,
|
||||
resource
|
||||
);
|
||||
|
||||
if (isAllowed) {
|
||||
logger.debug(
|
||||
"Resource allowed because user session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resourceSessionToken =
|
||||
sessions[
|
||||
`${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`
|
||||
`${config.getRawConfig().server.session_cookie_name}${resource.ssl ? "_s" : ""}`
|
||||
];
|
||||
|
||||
if (resourceSessionToken) {
|
||||
const { resourceSession } = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||
let resourceSession: any = cache.get(sessionCacheKey);
|
||||
|
||||
if (!resourceSession) {
|
||||
const result = await validateResourceSessionToken(
|
||||
resourceSessionToken,
|
||||
resource.resourceId
|
||||
);
|
||||
|
||||
resourceSession = result?.resourceSession;
|
||||
cache.set(sessionCacheKey, resourceSession);
|
||||
}
|
||||
|
||||
if (resourceSession?.isRequestToken) {
|
||||
logger.debug(
|
||||
"Resource not allowed because session is a temporary request token"
|
||||
);
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
if (resourceSession) {
|
||||
if (pincode && resourceSession.pincodeId) {
|
||||
|
@ -208,6 +234,29 @@ export async function verifyResourceSession(
|
|||
);
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
if (resourceSession.userSessionId && sso) {
|
||||
const userAccessCacheKey = `userAccess:${resourceSession.userSessionId}:${resource.resourceId}`;
|
||||
|
||||
let isAllowed: boolean | undefined =
|
||||
cache.get(userAccessCacheKey);
|
||||
|
||||
if (isAllowed === undefined) {
|
||||
isAllowed = await isUserAllowedToAccessResource(
|
||||
resourceSession.userSessionId,
|
||||
resource
|
||||
);
|
||||
|
||||
cache.set(userAccessCacheKey, isAllowed);
|
||||
}
|
||||
|
||||
if (isAllowed) {
|
||||
logger.debug(
|
||||
"Resource allowed because user session is valid"
|
||||
);
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,10 +321,15 @@ async function createAccessTokenSession(
|
|||
expiresAt: tokenItem.expiresAt,
|
||||
doNotExtend: tokenItem.expiresAt ? true : false
|
||||
});
|
||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain,
|
||||
token,
|
||||
!resource.ssl
|
||||
);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
logger.debug("Access token is valid, creating new session")
|
||||
logger.debug("Access token is valid, creating new session");
|
||||
return response<VerifyUserResponse>(res, {
|
||||
data: { valid: true },
|
||||
success: true,
|
||||
|
@ -286,9 +340,22 @@ async function createAccessTokenSession(
|
|||
}
|
||||
|
||||
async function isUserAllowedToAccessResource(
|
||||
user: User,
|
||||
userSessionId: string,
|
||||
resource: Resource
|
||||
): Promise<boolean> {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.leftJoin(users, eq(users.userId, sessions.userId))
|
||||
.where(eq(sessions.sessionId, userSessionId));
|
||||
|
||||
const user = res.user;
|
||||
const session = res.session;
|
||||
|
||||
if (!user || !session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
config.getRawConfig().flags?.require_email_verification &&
|
||||
!user.emailVerified
|
||||
|
|
|
@ -3,7 +3,9 @@ import * as gerbil from "@server/routers/gerbil";
|
|||
import * as badger from "@server/routers/badger";
|
||||
import * as traefik from "@server/routers/traefik";
|
||||
import * as auth from "@server/routers/auth";
|
||||
import * as resource from "@server/routers/resource";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
|
||||
|
||||
// Root routes
|
||||
const internalRouter = Router();
|
||||
|
@ -15,7 +17,14 @@ internalRouter.get("/", (_, res) => {
|
|||
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||
internalRouter.get(
|
||||
"/resource-session/:resourceId/:token",
|
||||
auth.checkResourceSession,
|
||||
auth.checkResourceSession
|
||||
);
|
||||
|
||||
internalRouter.post(
|
||||
`/resource/:resourceId/get-exchange-token`,
|
||||
verifySessionUserMiddleware,
|
||||
verifyResourceAccess,
|
||||
resource.getExchangeToken
|
||||
);
|
||||
|
||||
// Gerbil routes
|
||||
|
@ -30,5 +39,6 @@ const badgerRouter = Router();
|
|||
internalRouter.use("/badger", badgerRouter);
|
||||
|
||||
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||
|
||||
export default internalRouter;
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { resourceAccessToken, resources } from "@server/db/schema";
|
||||
import { resources } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { 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 {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie
|
||||
} from "@server/auth/sessions/resource";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
|
||||
|
@ -108,13 +106,11 @@ export async function authWithAccessToken(
|
|||
resourceId,
|
||||
token,
|
||||
accessTokenId: tokenItem.accessTokenId,
|
||||
sessionLength: tokenItem.sessionLength,
|
||||
expiresAt: tokenItem.expiresAt,
|
||||
doNotExtend: tokenItem.expiresAt ? true : false
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<AuthWithAccessTokenResponse>(res, {
|
||||
data: {
|
||||
|
|
|
@ -11,9 +11,7 @@ import { z } from "zod";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie
|
||||
} from "@server/auth/sessions/resource";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
|
||||
|
@ -120,11 +118,12 @@ export async function authWithPassword(
|
|||
await createResourceSession({
|
||||
resourceId,
|
||||
token,
|
||||
passwordId: definedPassword.passwordId
|
||||
passwordId: definedPassword.passwordId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<AuthWithPasswordResponse>(res, {
|
||||
data: {
|
||||
|
|
|
@ -1,28 +1,21 @@
|
|||
import { verify } from "@node-rs/argon2";
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import {
|
||||
orgs,
|
||||
resourceOtp,
|
||||
resourcePincode,
|
||||
resources,
|
||||
resourceWhitelist
|
||||
} from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { 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 {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie
|
||||
} from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { AuthWithPasswordResponse } from "./authWithPassword";
|
||||
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
|
||||
export const authWithPincodeBodySchema = z
|
||||
|
@ -128,11 +121,12 @@ export async function authWithPincode(
|
|||
await createResourceSession({
|
||||
resourceId,
|
||||
token,
|
||||
pincodeId: definedPincode.pincodeId
|
||||
pincodeId: definedPincode.pincodeId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<AuthWithPincodeResponse>(res, {
|
||||
data: {
|
||||
|
|
|
@ -3,7 +3,6 @@ import db from "@server/db";
|
|||
import {
|
||||
orgs,
|
||||
resourceOtp,
|
||||
resourcePassword,
|
||||
resources,
|
||||
resourceWhitelist
|
||||
} from "@server/db/schema";
|
||||
|
@ -16,9 +15,7 @@ import { z } from "zod";
|
|||
import { fromError } from "zod-validation-error";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie
|
||||
} from "@server/auth/sessions/resource";
|
||||
import config from "@server/lib/config";
|
||||
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
||||
import logger from "@server/logger";
|
||||
|
||||
|
@ -178,11 +175,12 @@ export async function authWithWhitelist(
|
|||
await createResourceSession({
|
||||
resourceId,
|
||||
token,
|
||||
whitelistId: whitelistedEmail.whitelistId
|
||||
whitelistId: whitelistedEmail.whitelistId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<AuthWithWhitelistResponse>(res, {
|
||||
data: {
|
||||
|
|
109
server/routers/resource/getExchangeToken.ts
Normal file
109
server/routers/resource/getExchangeToken.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db/schema";
|
||||
import { eq } 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 { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import config from "@server/lib/config";
|
||||
import {
|
||||
encodeHexLowerCase
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { response } from "@server/lib";
|
||||
|
||||
const getExchangeTokenParams = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type GetExchangeTokenResponse = {
|
||||
requestToken: string;
|
||||
};
|
||||
|
||||
export async function getExchangeToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getExchangeTokenParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const resource = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (resource.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with ID ${resourceId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ssoSession =
|
||||
req.cookies[config.getRawConfig().server.session_cookie_name];
|
||||
if (!ssoSession) {
|
||||
logger.debug(ssoSession);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"Missing SSO session cookie"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(ssoSession))
|
||||
);
|
||||
|
||||
const token = generateSessionToken();
|
||||
await createResourceSession({
|
||||
resourceId,
|
||||
token,
|
||||
userSessionId: sessionId,
|
||||
isRequestToken: true,
|
||||
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||
sessionLength: 1000 * 30,
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logger.debug("Request token created successfully");
|
||||
|
||||
return response<GetExchangeTokenResponse>(res, {
|
||||
data: {
|
||||
requestToken: token
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Request token created successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,3 +16,4 @@ export * from "./setResourceWhitelist";
|
|||
export * from "./getResourceWhitelist";
|
||||
export * from "./authWithWhitelist";
|
||||
export * from "./authWithAccessToken";
|
||||
export * from "./getExchangeToken";
|
||||
|
|
|
@ -111,6 +111,10 @@ export async function updateResource(
|
|||
);
|
||||
}
|
||||
|
||||
if (resource[0].resources.ssl !== updatedResource[0].ssl) {
|
||||
// invalidate all sessions?
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: updatedResource[0],
|
||||
success: true,
|
||||
|
|
|
@ -39,7 +39,7 @@ export async function traefikConfigProvider(
|
|||
}
|
||||
|
||||
const badgerMiddlewareName = "badger";
|
||||
const redirectMiddlewareName = "redirect-to-https";
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
|
||||
const http: any = {
|
||||
routers: {},
|
||||
|
@ -52,19 +52,18 @@ export async function traefikConfigProvider(
|
|||
"/api/v1",
|
||||
`http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
|
||||
).href,
|
||||
resourceSessionCookieName:
|
||||
config.getRawConfig().server.resource_session_cookie_name,
|
||||
userSessionCookieName:
|
||||
config.getRawConfig().server.session_cookie_name,
|
||||
accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
|
||||
resourceSessionRequestParam: config.getRawConfig().server.resource_session_request_param
|
||||
},
|
||||
},
|
||||
},
|
||||
[redirectMiddlewareName]: {
|
||||
[redirectHttpsMiddlewareName]: {
|
||||
redirectScheme: {
|
||||
scheme: "https"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
for (const item of all) {
|
||||
|
@ -120,10 +119,9 @@ export async function traefikConfigProvider(
|
|||
};
|
||||
|
||||
if (resource.ssl) {
|
||||
// this is a redirect router; all it does is redirect to the https version if tls is enabled
|
||||
http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||
middlewares: [redirectMiddlewareName],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
};
|
||||
|
|
|
@ -7,7 +7,13 @@ import {
|
|||
userInvites,
|
||||
users
|
||||
} from "@server/db/schema";
|
||||
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { sql } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
|
||||
export default async function migration() {
|
||||
console.log("Running setup script 1.0.0-beta.9...");
|
||||
|
@ -32,5 +38,96 @@ export default async function migration() {
|
|||
console.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine which config file exists
|
||||
const filePaths = [configFilePath1, configFilePath2];
|
||||
let filePath = "";
|
||||
for (const path of filePaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
filePath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error(
|
||||
`No config file found (expected config.yml or config.yaml).`
|
||||
);
|
||||
}
|
||||
|
||||
// Read and parse the YAML file
|
||||
let rawConfig: any;
|
||||
const fileContents = fs.readFileSync(filePath, "utf8");
|
||||
rawConfig = yaml.load(fileContents);
|
||||
|
||||
rawConfig.server.resource_session_request_param = "p_session_request";
|
||||
rawConfig.server.session_cookie_name = "p_session_token"; // rename to prevent conflicts
|
||||
delete rawConfig.server.resource_session_cookie_name;
|
||||
|
||||
// Write the updated YAML back to the file
|
||||
const updatedYaml = yaml.dump(rawConfig);
|
||||
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Failed to add resource_session_request_param to config. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const traefikPath = path.join(
|
||||
APP_PATH,
|
||||
"traefik",
|
||||
"traefik_config.yml"
|
||||
);
|
||||
|
||||
const schema = z.object({
|
||||
experimental: z.object({
|
||||
plugins: z.object({
|
||||
badger: z.object({
|
||||
moduleName: z.string(),
|
||||
version: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
|
||||
const traefikConfig = yaml.load(traefikFileContents) as any;
|
||||
|
||||
const parsedConfig = schema.safeParse(traefikConfig);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
throw new Error(fromZodError(parsedConfig.error).toString());
|
||||
}
|
||||
|
||||
traefikConfig.experimental.plugins.badger.version = "v1.0.0-beta.3";
|
||||
|
||||
const updatedTraefikYaml = yaml.dump(traefikConfig);
|
||||
|
||||
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
|
||||
|
||||
console.log(
|
||||
"Updated the version of Badger in your Traefik configuration to v1.0.0-beta.3."
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually."
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'isRequestToken' integer;`);
|
||||
trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'userSessionId' text REFERENCES session(id);`);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"We were unable to add columns to the resourceSessions table."
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log("Done.");
|
||||
}
|
||||
|
|
|
@ -5,14 +5,12 @@ import { Button } from "@app/components/ui/button";
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@app/components/ui/card";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { AuthWithAccessTokenResponse } from "@server/routers/resource";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
@ -32,7 +30,17 @@ export default function AccessToken({
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
|
||||
function appendRequestToken(url: string, token: string) {
|
||||
const fullUrl = new URL(url);
|
||||
fullUrl.searchParams.append(
|
||||
env.server.resourceSessionRequestParam,
|
||||
token
|
||||
);
|
||||
return fullUrl.toString();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessTokenId || !accessToken) {
|
||||
|
@ -51,7 +59,10 @@ export default function AccessToken({
|
|||
|
||||
if (res.data.data.session) {
|
||||
setIsValid(true);
|
||||
window.location.href = redirectUrl;
|
||||
window.location.href = appendRequestToken(
|
||||
redirectUrl,
|
||||
res.data.data.session
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking access token", e);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { useState } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
@ -8,7 +8,6 @@ import {
|
|||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
|
@ -30,9 +29,6 @@ import {
|
|||
Key,
|
||||
User,
|
||||
Send,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Lock,
|
||||
AtSign
|
||||
} from "lucide-react";
|
||||
import {
|
||||
|
@ -47,10 +43,8 @@ import { AxiosResponse } from "axios";
|
|||
import LoginForm from "@app/components/LoginForm";
|
||||
import {
|
||||
AuthWithPasswordResponse,
|
||||
AuthWithAccessTokenResponse,
|
||||
AuthWithWhitelistResponse
|
||||
} from "@server/routers/resource";
|
||||
import { redirect } from "next/dist/server/api-utils";
|
||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
@ -118,7 +112,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
|
||||
const [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle");
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const api = createApiClient({ env });
|
||||
|
||||
function getDefaultSelectedMethod() {
|
||||
if (props.methods.sso) {
|
||||
|
@ -169,6 +165,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
}
|
||||
});
|
||||
|
||||
function appendRequestToken(url: string, token: string) {
|
||||
const fullUrl = new URL(url);
|
||||
fullUrl.searchParams.append(
|
||||
env.server.resourceSessionRequestParam,
|
||||
token
|
||||
);
|
||||
return fullUrl.toString();
|
||||
}
|
||||
|
||||
const onWhitelistSubmit = (values: any) => {
|
||||
setLoadingLogin(true);
|
||||
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
|
||||
|
@ -190,7 +195,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
|
||||
const session = res.data.data.session;
|
||||
if (session) {
|
||||
window.location.href = props.redirect;
|
||||
window.location.href = appendRequestToken(props.redirect, session);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -212,7 +217,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
setPincodeError(null);
|
||||
const session = res.data.data.session;
|
||||
if (session) {
|
||||
window.location.href = props.redirect;
|
||||
window.location.href = appendRequestToken(props.redirect, session);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -237,7 +242,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
setPasswordError(null);
|
||||
const session = res.data.data.session;
|
||||
if (session) {
|
||||
window.location.href = props.redirect;
|
||||
window.location.href = appendRequestToken(props.redirect, session);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -619,16 +624,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* {activeTab === "sso" && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline">
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
) : (
|
||||
<ResourceAccessDenied />
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
AuthWithAccessTokenResponse,
|
||||
GetResourceAuthInfoResponse,
|
||||
GetResourceResponse
|
||||
GetExchangeTokenResponse
|
||||
} from "@server/routers/resource";
|
||||
import ResourceAuthPortal from "./ResourceAuthPortal";
|
||||
import { internal, priv } from "@app/lib/api";
|
||||
|
@ -12,9 +11,6 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
|||
import { redirect } from "next/navigation";
|
||||
import ResourceNotFound from "./ResourceNotFound";
|
||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||
import { cookies } from "next/headers";
|
||||
import { CheckResourceSessionResponse } from "@server/routers/auth";
|
||||
import AccessTokenInvalid from "./AccessToken";
|
||||
import AccessToken from "./AccessToken";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
|
||||
|
@ -48,7 +44,7 @@ export default async function ResourceAuthPage(props: {
|
|||
// TODO: fix this
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
{/* @ts-ignore */}
|
||||
{/* @ts-ignore */}
|
||||
<ResourceNotFound />
|
||||
</div>
|
||||
);
|
||||
|
@ -83,49 +79,41 @@ export default async function ResourceAuthPage(props: {
|
|||
);
|
||||
}
|
||||
|
||||
const allCookies = await cookies();
|
||||
const cookieName =
|
||||
env.server.resourceSessionCookieName + `_${params.resourceId}`;
|
||||
const sessionId = allCookies.get(cookieName)?.value ?? null;
|
||||
|
||||
if (sessionId) {
|
||||
let doRedirect = false;
|
||||
try {
|
||||
const res = await priv.get<
|
||||
AxiosResponse<CheckResourceSessionResponse>
|
||||
>(`/resource-session/${params.resourceId}/${sessionId}`);
|
||||
|
||||
if (res && res.data.data.valid) {
|
||||
doRedirect = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (doRedirect) {
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAuth) {
|
||||
// no authentication so always go straight to the resource
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
|
||||
|
||||
// convert the dashboard token into a resource session token
|
||||
let userIsUnauthorized = false;
|
||||
if (user && authInfo.sso) {
|
||||
let doRedirect = false;
|
||||
let redirectToUrl: string | undefined;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
|
||||
`/resource/${params.resourceId}`,
|
||||
const res = await priv.post<
|
||||
AxiosResponse<GetExchangeTokenResponse>
|
||||
>(
|
||||
`/resource/${params.resourceId}/get-exchange-token`,
|
||||
{},
|
||||
await authCookieHeader()
|
||||
);
|
||||
|
||||
doRedirect = true;
|
||||
if (res.data.data.requestToken) {
|
||||
const paramName = env.server.resourceSessionRequestParam;
|
||||
// append the param with the token to the redirect url
|
||||
const fullUrl = new URL(redirectUrl);
|
||||
fullUrl.searchParams.append(
|
||||
paramName,
|
||||
res.data.data.requestToken
|
||||
);
|
||||
redirectToUrl = fullUrl.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
userIsUnauthorized = true;
|
||||
}
|
||||
|
||||
if (doRedirect) {
|
||||
redirect(redirectUrl);
|
||||
if (redirectToUrl) {
|
||||
redirect(redirectToUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ export function pullEnv(): Env {
|
|||
nextPort: process.env.NEXT_PORT as string,
|
||||
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
||||
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
|
||||
resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string,
|
||||
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string
|
||||
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
||||
resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
|
||||
},
|
||||
app: {
|
||||
environment: process.env.ENVIRONMENT as string,
|
||||
|
|
|
@ -7,8 +7,8 @@ export type Env = {
|
|||
externalPort: string;
|
||||
nextPort: string;
|
||||
sessionCookieName: string;
|
||||
resourceSessionCookieName: string;
|
||||
resourceAccessTokenParam: string;
|
||||
resourceSessionRequestParam: string;
|
||||
},
|
||||
email: {
|
||||
emailEnabled: boolean;
|
||||
|
|
Loading…
Add table
Reference in a new issue