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
|
next_port: 3002
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
secure_cookies: true
|
secure_cookies: true
|
||||||
session_cookie_name: "p_session"
|
session_cookie_name: "p_session_token"
|
||||||
resource_session_cookie_name: "p_resource_session"
|
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
|
resource_session_request_param: "p_session_request"
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
cert_resolver: "letsencrypt"
|
cert_resolver: "letsencrypt"
|
||||||
|
|
|
@ -10,9 +10,9 @@ server:
|
||||||
next_port: 3002
|
next_port: 3002
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
secure_cookies: true
|
secure_cookies: true
|
||||||
session_cookie_name: "p_session"
|
session_cookie_name: "p_session_token"
|
||||||
resource_session_cookie_name: "p_resource_session"
|
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
|
resource_session_request_param: "p_session_request"
|
||||||
cors:
|
cors:
|
||||||
origins: ["https://{{.DashboardDomain}}"]
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"next-themes": "0.4.4",
|
"next-themes": "0.4.4",
|
||||||
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "6.9.16",
|
"nodemailer": "6.9.16",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
|
|
|
@ -3,7 +3,13 @@ import {
|
||||||
encodeHexLowerCase
|
encodeHexLowerCase
|
||||||
} from "@oslojs/encoding";
|
} from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
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 db from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
@ -13,9 +19,14 @@ import logger from "@server/logger";
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.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 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 {
|
export function generateSessionToken(): string {
|
||||||
const bytes = new Uint8Array(20);
|
const bytes = new Uint8Array(20);
|
||||||
|
@ -65,12 +76,21 @@ export async function validateSessionToken(
|
||||||
session.expiresAt = new Date(
|
session.expiresAt = new Date(
|
||||||
Date.now() + SESSION_COOKIE_EXPIRES
|
Date.now() + SESSION_COOKIE_EXPIRES
|
||||||
).getTime();
|
).getTime();
|
||||||
await db
|
await db.transaction(async (trx) => {
|
||||||
.update(sessions)
|
await trx
|
||||||
.set({
|
.update(sessions)
|
||||||
expiresAt: session.expiresAt
|
.set({
|
||||||
})
|
expiresAt: session.expiresAt
|
||||||
.where(eq(sessions.sessionId, session.sessionId));
|
})
|
||||||
|
.where(eq(sessions.sessionId, session.sessionId));
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.update(resourceSessions)
|
||||||
|
.set({
|
||||||
|
expiresAt: session.expiresAt
|
||||||
|
})
|
||||||
|
.where(eq(resourceSessions.userSessionId, session.sessionId));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { session, user };
|
return { session, user };
|
||||||
}
|
}
|
||||||
|
@ -90,9 +110,9 @@ export function serializeSessionCookie(
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
logger.debug("Setting cookie for secure origin");
|
logger.debug("Setting cookie for secure origin");
|
||||||
if (SECURE_COOKIES) {
|
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 {
|
} 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 {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
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";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.resource_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.resource_session_length_hours;
|
||||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
||||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
|
||||||
|
|
||||||
export async function createResourceSession(opts: {
|
export async function createResourceSession(opts: {
|
||||||
token: string;
|
token: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
passwordId?: number;
|
isRequestToken?: boolean;
|
||||||
pincodeId?: number;
|
passwordId?: number | null;
|
||||||
whitelistId?: number;
|
pincodeId?: number | null;
|
||||||
accessTokenId?: string;
|
userSessionId?: string | null;
|
||||||
usedOtp?: boolean;
|
whitelistId?: number | null;
|
||||||
|
accessTokenId?: string | null;
|
||||||
doNotExtend?: boolean;
|
doNotExtend?: boolean;
|
||||||
expiresAt?: number | null;
|
expiresAt?: number | null;
|
||||||
sessionLength?: number | null;
|
sessionLength?: number | null;
|
||||||
|
@ -27,7 +28,8 @@ export async function createResourceSession(opts: {
|
||||||
!opts.passwordId &&
|
!opts.passwordId &&
|
||||||
!opts.pincodeId &&
|
!opts.pincodeId &&
|
||||||
!opts.whitelistId &&
|
!opts.whitelistId &&
|
||||||
!opts.accessTokenId
|
!opts.accessTokenId &&
|
||||||
|
!opts.userSessionId
|
||||||
) {
|
) {
|
||||||
throw new Error("Auth method must be provided");
|
throw new Error("Auth method must be provided");
|
||||||
}
|
}
|
||||||
|
@ -47,7 +49,9 @@ export async function createResourceSession(opts: {
|
||||||
pincodeId: opts.pincodeId || null,
|
pincodeId: opts.pincodeId || null,
|
||||||
whitelistId: opts.whitelistId || null,
|
whitelistId: opts.whitelistId || null,
|
||||||
doNotExtend: opts.doNotExtend || false,
|
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);
|
await db.insert(resourceSessions).values(session);
|
||||||
|
@ -162,22 +166,25 @@ export async function invalidateAllSessions(
|
||||||
|
|
||||||
export function serializeResourceSessionCookie(
|
export function serializeResourceSessionCookie(
|
||||||
cookieName: string,
|
cookieName: string,
|
||||||
token: string
|
domain: string,
|
||||||
|
token: string,
|
||||||
|
isHttp: boolean = false
|
||||||
): string {
|
): string {
|
||||||
if (SECURE_COOKIES) {
|
if (SECURE_COOKIES && !isHttp) {
|
||||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
|
||||||
} else {
|
} 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(
|
export function createBlankResourceSessionTokenCookie(
|
||||||
cookieName: string
|
cookieName: string,
|
||||||
|
domain: string
|
||||||
): string {
|
): string {
|
||||||
if (SECURE_COOKIES) {
|
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 {
|
} 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" })
|
doNotExtend: integer("doNotExtend", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
isRequestToken: integer("isRequestToken", { mode: "boolean" }),
|
||||||
|
userSessionId: text("userSessionId").references(() => sessions.sessionId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
passwordId: integer("passwordId").references(
|
passwordId: integer("passwordId").references(
|
||||||
() => resourcePassword.passwordId,
|
() => resourcePassword.passwordId,
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { runSetupFunctions } from "./setup";
|
||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { User, UserOrg } from "./db/schema";
|
import { Session, User, UserOrg } from "./db/schema";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
@ -24,6 +24,7 @@ declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
user?: User;
|
user?: User;
|
||||||
|
session?: Session;
|
||||||
userOrg?: UserOrg;
|
userOrg?: UserOrg;
|
||||||
userOrgRoleId?: number;
|
userOrgRoleId?: number;
|
||||||
userOrgId?: string;
|
userOrgId?: string;
|
||||||
|
|
|
@ -61,8 +61,20 @@ const configSchema = z.object({
|
||||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
secure_cookies: z.boolean(),
|
secure_cookies: z.boolean(),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_session_cookie_name: z.string(),
|
|
||||||
resource_access_token_param: 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
|
cors: z
|
||||||
.object({
|
.object({
|
||||||
origins: z.array(z.string()).optional(),
|
origins: z.array(z.string()).optional(),
|
||||||
|
@ -241,8 +253,6 @@ export class Config {
|
||||||
: "false";
|
: "false";
|
||||||
process.env.SESSION_COOKIE_NAME =
|
process.env.SESSION_COOKIE_NAME =
|
||||||
parsedConfig.data.server.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.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||||
?.disable_signup_without_invite
|
?.disable_signup_without_invite
|
||||||
|
@ -254,6 +264,8 @@ export class Config {
|
||||||
: "false";
|
: "false";
|
||||||
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
||||||
parsedConfig.data.server.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;
|
this.rawConfig = parsedConfig.data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,17 @@ import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
createBlankSessionTokenCookie,
|
createBlankSessionTokenCookie,
|
||||||
invalidateSession,
|
invalidateSession
|
||||||
SESSION_COOKIE_NAME
|
|
||||||
} from "@server/auth/sessions/app";
|
} from "@server/auth/sessions/app";
|
||||||
|
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||||
|
|
||||||
export async function logout(
|
export async function logout(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const sessionId = req.cookies[SESSION_COOKIE_NAME];
|
const { user, session } = await verifySession(req);
|
||||||
|
if (!user || !session) {
|
||||||
if (!sessionId) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
@ -26,7 +25,7 @@ export async function logout(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invalidateSession(sessionId);
|
await invalidateSession(session.sessionId);
|
||||||
const isSecure = req.protocol === "https";
|
const isSecure = req.protocol === "https";
|
||||||
res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure));
|
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 "./verifySession";
|
||||||
|
export * from "./exchangeSession";
|
||||||
|
|
|
@ -4,17 +4,17 @@ import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
import {
|
import {
|
||||||
ResourceAccessToken,
|
ResourceAccessToken,
|
||||||
resourceAccessToken,
|
ResourcePassword,
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
|
ResourcePincode,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resources,
|
resources,
|
||||||
resourceWhitelist,
|
sessions,
|
||||||
User,
|
userOrgs,
|
||||||
userOrgs
|
users
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
@ -27,6 +27,12 @@ import { Resource, roleResources, userResources } from "@server/db/schema";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import { generateSessionToken } from "@server/auth";
|
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({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string()).optional(),
|
sessions: z.record(z.string()).optional(),
|
||||||
|
@ -53,7 +59,7 @@ export async function verifyResourceSession(
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): 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);
|
const parsedBody = verifyResourceSessionSchema.safeParse(req.body);
|
||||||
|
|
||||||
|
@ -67,26 +73,52 @@ export async function verifyResourceSession(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { sessions, host, originalRequestURL, accessToken: token } =
|
const {
|
||||||
parsedBody.data;
|
sessions,
|
||||||
|
host,
|
||||||
|
originalRequestURL,
|
||||||
|
accessToken: token
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
const [result] = await db
|
const resourceCacheKey = `resource:${host}`;
|
||||||
.select()
|
let resourceData:
|
||||||
.from(resources)
|
| {
|
||||||
.leftJoin(
|
resource: Resource | null;
|
||||||
resourcePincode,
|
pincode: ResourcePincode | null;
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
password: ResourcePassword | null;
|
||||||
)
|
}
|
||||||
.leftJoin(
|
| undefined = cache.get(resourceCacheKey);
|
||||||
resourcePassword,
|
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.where(eq(resources.fullDomain, host))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const resource = result?.resources;
|
if (!resourceData) {
|
||||||
const pincode = result?.resourcePincode;
|
const [result] = await db
|
||||||
const password = result?.resourcePassword;
|
.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) {
|
if (!resource) {
|
||||||
logger.debug("Resource not found", host);
|
logger.debug("Resource not found", host);
|
||||||
|
@ -145,37 +177,31 @@ export async function verifyResourceSession(
|
||||||
return notAllowed(res);
|
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 =
|
const resourceSessionToken =
|
||||||
sessions[
|
sessions[
|
||||||
`${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`
|
`${config.getRawConfig().server.session_cookie_name}${resource.ssl ? "_s" : ""}`
|
||||||
];
|
];
|
||||||
|
|
||||||
if (resourceSessionToken) {
|
if (resourceSessionToken) {
|
||||||
const { resourceSession } = await validateResourceSessionToken(
|
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||||
resourceSessionToken,
|
let resourceSession: any = cache.get(sessionCacheKey);
|
||||||
resource.resourceId
|
|
||||||
);
|
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 (resourceSession) {
|
||||||
if (pincode && resourceSession.pincodeId) {
|
if (pincode && resourceSession.pincodeId) {
|
||||||
|
@ -208,6 +234,29 @@ export async function verifyResourceSession(
|
||||||
);
|
);
|
||||||
return allowed(res);
|
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,
|
expiresAt: tokenItem.expiresAt,
|
||||||
doNotExtend: tokenItem.expiresAt ? true : false
|
doNotExtend: tokenItem.expiresAt ? true : false
|
||||||
});
|
});
|
||||||
const cookieName = `${config.getRawConfig().server.resource_session_cookie_name}_${resource.resourceId}`;
|
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||||
const cookie = serializeResourceSessionCookie(cookieName, token);
|
const cookie = serializeResourceSessionCookie(
|
||||||
|
cookieName,
|
||||||
|
resource.fullDomain,
|
||||||
|
token,
|
||||||
|
!resource.ssl
|
||||||
|
);
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
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, {
|
return response<VerifyUserResponse>(res, {
|
||||||
data: { valid: true },
|
data: { valid: true },
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -286,9 +340,22 @@ async function createAccessTokenSession(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isUserAllowedToAccessResource(
|
async function isUserAllowedToAccessResource(
|
||||||
user: User,
|
userSessionId: string,
|
||||||
resource: Resource
|
resource: Resource
|
||||||
): Promise<boolean> {
|
): 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 (
|
if (
|
||||||
config.getRawConfig().flags?.require_email_verification &&
|
config.getRawConfig().flags?.require_email_verification &&
|
||||||
!user.emailVerified
|
!user.emailVerified
|
||||||
|
|
|
@ -3,7 +3,9 @@ import * as gerbil from "@server/routers/gerbil";
|
||||||
import * as badger from "@server/routers/badger";
|
import * as badger from "@server/routers/badger";
|
||||||
import * as traefik from "@server/routers/traefik";
|
import * as traefik from "@server/routers/traefik";
|
||||||
import * as auth from "@server/routers/auth";
|
import * as auth from "@server/routers/auth";
|
||||||
|
import * as resource from "@server/routers/resource";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
const internalRouter = Router();
|
const internalRouter = Router();
|
||||||
|
@ -15,7 +17,14 @@ internalRouter.get("/", (_, res) => {
|
||||||
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||||
internalRouter.get(
|
internalRouter.get(
|
||||||
"/resource-session/:resourceId/:token",
|
"/resource-session/:resourceId/:token",
|
||||||
auth.checkResourceSession,
|
auth.checkResourceSession
|
||||||
|
);
|
||||||
|
|
||||||
|
internalRouter.post(
|
||||||
|
`/resource/:resourceId/get-exchange-token`,
|
||||||
|
verifySessionUserMiddleware,
|
||||||
|
verifyResourceAccess,
|
||||||
|
resource.getExchangeToken
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gerbil routes
|
// Gerbil routes
|
||||||
|
@ -30,5 +39,6 @@ const badgerRouter = Router();
|
||||||
internalRouter.use("/badger", badgerRouter);
|
internalRouter.use("/badger", badgerRouter);
|
||||||
|
|
||||||
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||||
|
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||||
|
|
||||||
export default internalRouter;
|
export default internalRouter;
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import db from "@server/db";
|
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 HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createResourceSession,
|
createResourceSession,
|
||||||
serializeResourceSessionCookie
|
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
|
|
||||||
|
@ -108,13 +106,11 @@ export async function authWithAccessToken(
|
||||||
resourceId,
|
resourceId,
|
||||||
token,
|
token,
|
||||||
accessTokenId: tokenItem.accessTokenId,
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
sessionLength: tokenItem.sessionLength,
|
isRequestToken: true,
|
||||||
expiresAt: tokenItem.expiresAt,
|
expiresAt: Date.now() + 1000 * 30, // 30 seconds
|
||||||
doNotExtend: tokenItem.expiresAt ? true : false
|
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, {
|
return response<AuthWithAccessTokenResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -11,9 +11,7 @@ import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createResourceSession,
|
createResourceSession,
|
||||||
serializeResourceSessionCookie
|
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
|
|
||||||
|
@ -120,11 +118,12 @@ export async function authWithPassword(
|
||||||
await createResourceSession({
|
await createResourceSession({
|
||||||
resourceId,
|
resourceId,
|
||||||
token,
|
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, {
|
return response<AuthWithPasswordResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
import { verify } from "@node-rs/argon2";
|
|
||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
import {
|
import {
|
||||||
orgs,
|
orgs,
|
||||||
resourceOtp,
|
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resources,
|
resources,
|
||||||
resourceWhitelist
|
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createResourceSession,
|
createResourceSession,
|
||||||
serializeResourceSessionCookie
|
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import logger from "@server/logger";
|
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";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
|
|
||||||
export const authWithPincodeBodySchema = z
|
export const authWithPincodeBodySchema = z
|
||||||
|
@ -128,11 +121,12 @@ export async function authWithPincode(
|
||||||
await createResourceSession({
|
await createResourceSession({
|
||||||
resourceId,
|
resourceId,
|
||||||
token,
|
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, {
|
return response<AuthWithPincodeResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import db from "@server/db";
|
||||||
import {
|
import {
|
||||||
orgs,
|
orgs,
|
||||||
resourceOtp,
|
resourceOtp,
|
||||||
resourcePassword,
|
|
||||||
resources,
|
resources,
|
||||||
resourceWhitelist
|
resourceWhitelist
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
|
@ -16,9 +15,7 @@ import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createResourceSession,
|
createResourceSession,
|
||||||
serializeResourceSessionCookie
|
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
@ -178,11 +175,12 @@ export async function authWithWhitelist(
|
||||||
await createResourceSession({
|
await createResourceSession({
|
||||||
resourceId,
|
resourceId,
|
||||||
token,
|
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, {
|
return response<AuthWithWhitelistResponse>(res, {
|
||||||
data: {
|
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 "./getResourceWhitelist";
|
||||||
export * from "./authWithWhitelist";
|
export * from "./authWithWhitelist";
|
||||||
export * from "./authWithAccessToken";
|
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, {
|
return response(res, {
|
||||||
data: updatedResource[0],
|
data: updatedResource[0],
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
@ -39,7 +39,7 @@ export async function traefikConfigProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
const badgerMiddlewareName = "badger";
|
const badgerMiddlewareName = "badger";
|
||||||
const redirectMiddlewareName = "redirect-to-https";
|
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||||
|
|
||||||
const http: any = {
|
const http: any = {
|
||||||
routers: {},
|
routers: {},
|
||||||
|
@ -52,19 +52,18 @@ export async function traefikConfigProvider(
|
||||||
"/api/v1",
|
"/api/v1",
|
||||||
`http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
|
`http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
|
||||||
).href,
|
).href,
|
||||||
resourceSessionCookieName:
|
|
||||||
config.getRawConfig().server.resource_session_cookie_name,
|
|
||||||
userSessionCookieName:
|
userSessionCookieName:
|
||||||
config.getRawConfig().server.session_cookie_name,
|
config.getRawConfig().server.session_cookie_name,
|
||||||
accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
|
accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
|
||||||
|
resourceSessionRequestParam: config.getRawConfig().server.resource_session_request_param
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[redirectMiddlewareName]: {
|
[redirectHttpsMiddlewareName]: {
|
||||||
redirectScheme: {
|
redirectScheme: {
|
||||||
scheme: "https"
|
scheme: "https"
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
for (const item of all) {
|
for (const item of all) {
|
||||||
|
@ -120,10 +119,9 @@ export async function traefikConfigProvider(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resource.ssl) {
|
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"] = {
|
http.routers![routerName + "-redirect"] = {
|
||||||
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||||
middlewares: [redirectMiddlewareName],
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,13 @@ import {
|
||||||
userInvites,
|
userInvites,
|
||||||
users
|
users
|
||||||
} from "@server/db/schema";
|
} from "@server/db/schema";
|
||||||
|
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
import { sql } from "drizzle-orm";
|
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() {
|
export default async function migration() {
|
||||||
console.log("Running setup script 1.0.0-beta.9...");
|
console.log("Running setup script 1.0.0-beta.9...");
|
||||||
|
@ -32,5 +38,96 @@ export default async function migration() {
|
||||||
console.error(error);
|
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.");
|
console.log("Done.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,12 @@ import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { AuthWithAccessTokenResponse } from "@server/routers/resource";
|
import { AuthWithAccessTokenResponse } from "@server/routers/resource";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
@ -32,7 +30,17 @@ export default function AccessToken({
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isValid, setIsValid] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!accessTokenId || !accessToken) {
|
if (!accessTokenId || !accessToken) {
|
||||||
|
@ -51,7 +59,10 @@ export default function AccessToken({
|
||||||
|
|
||||||
if (res.data.data.session) {
|
if (res.data.data.session) {
|
||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
window.location.href = redirectUrl;
|
window.location.href = appendRequestToken(
|
||||||
|
redirectUrl,
|
||||||
|
res.data.data.session
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error checking access token", e);
|
console.error("Error checking access token", e);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
import { useState } from "react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
@ -8,7 +8,6 @@ import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
@ -30,9 +29,6 @@ import {
|
||||||
Key,
|
Key,
|
||||||
User,
|
User,
|
||||||
Send,
|
Send,
|
||||||
ArrowLeft,
|
|
||||||
ArrowRight,
|
|
||||||
Lock,
|
|
||||||
AtSign
|
AtSign
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
@ -47,10 +43,8 @@ import { AxiosResponse } from "axios";
|
||||||
import LoginForm from "@app/components/LoginForm";
|
import LoginForm from "@app/components/LoginForm";
|
||||||
import {
|
import {
|
||||||
AuthWithPasswordResponse,
|
AuthWithPasswordResponse,
|
||||||
AuthWithAccessTokenResponse,
|
|
||||||
AuthWithWhitelistResponse
|
AuthWithWhitelistResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import { redirect } from "next/dist/server/api-utils";
|
|
||||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
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 [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle");
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
function getDefaultSelectedMethod() {
|
function getDefaultSelectedMethod() {
|
||||||
if (props.methods.sso) {
|
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) => {
|
const onWhitelistSubmit = (values: any) => {
|
||||||
setLoadingLogin(true);
|
setLoadingLogin(true);
|
||||||
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
|
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
|
||||||
|
@ -190,7 +195,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
|
|
||||||
const session = res.data.data.session;
|
const session = res.data.data.session;
|
||||||
if (session) {
|
if (session) {
|
||||||
window.location.href = props.redirect;
|
window.location.href = appendRequestToken(props.redirect, session);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -212,7 +217,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
setPincodeError(null);
|
setPincodeError(null);
|
||||||
const session = res.data.data.session;
|
const session = res.data.data.session;
|
||||||
if (session) {
|
if (session) {
|
||||||
window.location.href = props.redirect;
|
window.location.href = appendRequestToken(props.redirect, session);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -237,7 +242,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
const session = res.data.data.session;
|
const session = res.data.data.session;
|
||||||
if (session) {
|
if (session) {
|
||||||
window.location.href = props.redirect;
|
window.location.href = appendRequestToken(props.redirect, session);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -619,16 +624,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ResourceAccessDenied />
|
<ResourceAccessDenied />
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
AuthWithAccessTokenResponse,
|
|
||||||
GetResourceAuthInfoResponse,
|
GetResourceAuthInfoResponse,
|
||||||
GetResourceResponse
|
GetExchangeTokenResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import ResourceAuthPortal from "./ResourceAuthPortal";
|
import ResourceAuthPortal from "./ResourceAuthPortal";
|
||||||
import { internal, priv } from "@app/lib/api";
|
import { internal, priv } from "@app/lib/api";
|
||||||
|
@ -12,9 +11,6 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import ResourceNotFound from "./ResourceNotFound";
|
import ResourceNotFound from "./ResourceNotFound";
|
||||||
import ResourceAccessDenied from "./ResourceAccessDenied";
|
import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { CheckResourceSessionResponse } from "@server/routers/auth";
|
|
||||||
import AccessTokenInvalid from "./AccessToken";
|
|
||||||
import AccessToken from "./AccessToken";
|
import AccessToken from "./AccessToken";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
|
||||||
|
@ -48,7 +44,7 @@ export default async function ResourceAuthPage(props: {
|
||||||
// TODO: fix this
|
// TODO: fix this
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<ResourceNotFound />
|
<ResourceNotFound />
|
||||||
</div>
|
</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) {
|
if (!hasAuth) {
|
||||||
// no authentication so always go straight to the resource
|
// no authentication so always go straight to the resource
|
||||||
redirect(redirectUrl);
|
redirect(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// convert the dashboard token into a resource session token
|
||||||
let userIsUnauthorized = false;
|
let userIsUnauthorized = false;
|
||||||
if (user && authInfo.sso) {
|
if (user && authInfo.sso) {
|
||||||
let doRedirect = false;
|
let redirectToUrl: string | undefined;
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
|
const res = await priv.post<
|
||||||
`/resource/${params.resourceId}`,
|
AxiosResponse<GetExchangeTokenResponse>
|
||||||
|
>(
|
||||||
|
`/resource/${params.resourceId}/get-exchange-token`,
|
||||||
|
{},
|
||||||
await authCookieHeader()
|
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) {
|
} catch (e) {
|
||||||
userIsUnauthorized = true;
|
userIsUnauthorized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doRedirect) {
|
if (redirectToUrl) {
|
||||||
redirect(redirectUrl);
|
redirect(redirectToUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ export function pullEnv(): Env {
|
||||||
nextPort: process.env.NEXT_PORT as string,
|
nextPort: process.env.NEXT_PORT as string,
|
||||||
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
||||||
sessionCookieName: process.env.SESSION_COOKIE_NAME 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: {
|
app: {
|
||||||
environment: process.env.ENVIRONMENT as string,
|
environment: process.env.ENVIRONMENT as string,
|
||||||
|
|
|
@ -7,8 +7,8 @@ export type Env = {
|
||||||
externalPort: string;
|
externalPort: string;
|
||||||
nextPort: string;
|
nextPort: string;
|
||||||
sessionCookieName: string;
|
sessionCookieName: string;
|
||||||
resourceSessionCookieName: string;
|
|
||||||
resourceAccessTokenParam: string;
|
resourceAccessTokenParam: string;
|
||||||
|
resourceSessionRequestParam: string;
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
|
Loading…
Add table
Reference in a new issue