mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-20 18:54:52 +02:00
add pass access token in headers
This commit is contained in:
parent
74d6b3d902
commit
6cc4bc2645
14 changed files with 333 additions and 161 deletions
|
@ -11,10 +11,12 @@ import { verifyPassword } from "./password";
|
|||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
|
||||
export async function verifyResourceAccessTokenSHA256({
|
||||
accessToken
|
||||
export async function verifyResourceAccessToken({
|
||||
accessToken,
|
||||
accessTokenId
|
||||
}: {
|
||||
accessToken: string;
|
||||
accessTokenId?: string;
|
||||
}): Promise<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
|
@ -25,17 +27,61 @@ export async function verifyResourceAccessTokenSHA256({
|
|||
sha256(new TextEncoder().encode(accessToken))
|
||||
);
|
||||
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.tokenHash, accessTokenHash)))
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
);
|
||||
let tokenItem: ResourceAccessToken | undefined;
|
||||
let resource: Resource | undefined;
|
||||
|
||||
const tokenItem = res?.resourceAccessToken;
|
||||
const resource = res?.resources;
|
||||
if (!accessTokenId) {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.tokenHash, accessTokenHash)))
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
);
|
||||
|
||||
tokenItem = res?.resourceAccessToken;
|
||||
resource = res?.resources;
|
||||
} else {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)))
|
||||
.innerJoin(
|
||||
resources,
|
||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||
);
|
||||
|
||||
if (res && res.resourceAccessToken) {
|
||||
if (res.resourceAccessToken.tokenHash?.startsWith("$argon")) {
|
||||
const validCode = await verifyPassword(
|
||||
accessToken,
|
||||
res.resourceAccessToken.tokenHash
|
||||
);
|
||||
|
||||
if (!validCode) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid access token"
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const tokenHash = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(accessToken))
|
||||
);
|
||||
|
||||
if (res.resourceAccessToken.tokenHash !== tokenHash) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid access token"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenItem = res?.resourceAccessToken;
|
||||
resource = res?.resources;
|
||||
}
|
||||
|
||||
if (!tokenItem || !resource) {
|
||||
return {
|
||||
|
@ -60,61 +106,3 @@ export async function verifyResourceAccessTokenSHA256({
|
|||
resource
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifyResourceAccessToken({
|
||||
resource,
|
||||
accessTokenId,
|
||||
accessToken
|
||||
}: {
|
||||
resource: Resource;
|
||||
accessTokenId: string;
|
||||
accessToken: string;
|
||||
}): Promise<{
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
tokenItem?: ResourceAccessToken;
|
||||
}> {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(resourceAccessToken)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceAccessToken.resourceId, resource.resourceId),
|
||||
eq(resourceAccessToken.accessTokenId, accessTokenId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const tokenItem = result;
|
||||
|
||||
if (!tokenItem) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Access token does not exist for resource"
|
||||
};
|
||||
}
|
||||
|
||||
const validCode = await verifyPassword(accessToken, tokenItem.tokenHash);
|
||||
|
||||
if (!validCode) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid access token"
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
tokenItem.expiresAt &&
|
||||
!isWithinExpirationDate(new Date(tokenItem.expiresAt))
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Access token has expired"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tokenItem
|
||||
};
|
||||
}
|
||||
|
|
|
@ -66,6 +66,10 @@ const configSchema = z.object({
|
|||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z.string(),
|
||||
resource_access_token_param: z.string(),
|
||||
resource_access_token_headers: z.object({
|
||||
id: z.string(),
|
||||
token: z.string()
|
||||
}),
|
||||
resource_session_request_param: z.string(),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
|
@ -239,6 +243,10 @@ export class Config {
|
|||
: "false";
|
||||
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
||||
parsedConfig.data.server.resource_access_token_param;
|
||||
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID =
|
||||
parsedConfig.data.server.resource_access_token_headers.id;
|
||||
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
|
||||
parsedConfig.data.server.resource_access_token_headers.token;
|
||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||
parsedConfig.data.server.resource_session_request_param;
|
||||
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
|
||||
|
@ -335,13 +343,13 @@ export class Config {
|
|||
|
||||
// update the supporter key in the database
|
||||
await db
|
||||
.update(supporterKey)
|
||||
.set({
|
||||
tier: data.data.tier || null,
|
||||
phrase: data.data.cutePhrase || null,
|
||||
valid: true
|
||||
})
|
||||
.where(eq(supporterKey.keyId, key.keyId));
|
||||
.update(supporterKey)
|
||||
.set({
|
||||
tier: data.data.tier || null,
|
||||
phrase: data.data.cutePhrase || null,
|
||||
valid: true
|
||||
})
|
||||
.where(eq(supporterKey.keyId, key.keyId));
|
||||
} catch (e) {
|
||||
this.supporterData = key;
|
||||
console.error("Failed to validate supporter key", e);
|
||||
|
|
|
@ -41,12 +41,13 @@ const cache = new NodeCache({
|
|||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string()).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
query: z.record(z.string()).optional(),
|
||||
originalRequestURL: z.string().url(),
|
||||
scheme: z.string(),
|
||||
host: z.string(),
|
||||
path: z.string(),
|
||||
method: z.string(),
|
||||
accessToken: z.string().optional(),
|
||||
tls: z.boolean(),
|
||||
requestIp: z.string().optional()
|
||||
});
|
||||
|
@ -85,7 +86,8 @@ export async function verifyResourceSession(
|
|||
originalRequestURL,
|
||||
requestIp,
|
||||
path,
|
||||
accessToken: token
|
||||
headers,
|
||||
query
|
||||
} = parsedBody.data;
|
||||
|
||||
const clientIp = requestIp?.split(":")[0];
|
||||
|
@ -183,12 +185,32 @@ export async function verifyResourceSession(
|
|||
resource.resourceId
|
||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||
|
||||
// check for access token
|
||||
let validAccessToken: ResourceAccessToken | undefined;
|
||||
if (token) {
|
||||
const [accessTokenId, accessToken] = token.split(".");
|
||||
// check for access token in headers
|
||||
if (
|
||||
headers &&
|
||||
headers[
|
||||
config.getRawConfig().server.resource_access_token_headers.id
|
||||
] &&
|
||||
headers[
|
||||
config.getRawConfig().server.resource_access_token_headers.token
|
||||
]
|
||||
) {
|
||||
const accessTokenId =
|
||||
headers[
|
||||
config.getRawConfig().server.resource_access_token_headers
|
||||
.id
|
||||
];
|
||||
const accessToken =
|
||||
headers[
|
||||
config.getRawConfig().server.resource_access_token_headers
|
||||
.token
|
||||
];
|
||||
|
||||
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||
{ resource, accessTokenId, accessToken }
|
||||
{
|
||||
accessToken,
|
||||
accessTokenId
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
|
@ -206,16 +228,43 @@ export async function verifyResourceSession(
|
|||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
validAccessToken = tokenItem;
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessions) {
|
||||
return await createAccessTokenSession(
|
||||
res,
|
||||
resource,
|
||||
tokenItem
|
||||
if (
|
||||
query &&
|
||||
query[config.getRawConfig().server.resource_access_token_param]
|
||||
) {
|
||||
const token =
|
||||
query[config.getRawConfig().server.resource_access_token_param];
|
||||
|
||||
const [accessTokenId, accessToken] = token.split(".");
|
||||
|
||||
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||
{
|
||||
accessToken,
|
||||
accessTokenId
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
logger.debug("Access token invalid: " + error);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Resource access token is invalid. Resource ID: ${
|
||||
resource.resourceId
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessions) {
|
||||
|
@ -321,16 +370,6 @@ export async function verifyResourceSession(
|
|||
}
|
||||
}
|
||||
|
||||
// At this point we have checked all sessions, but since the access token is
|
||||
// valid, we should allow access and create a new session.
|
||||
if (validAccessToken) {
|
||||
return await createAccessTokenSession(
|
||||
res,
|
||||
resource,
|
||||
validAccessToken
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug("No more auth to check, resource not allowed");
|
||||
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
|
@ -360,8 +399,7 @@ function extractResourceSessionToken(
|
|||
ssl ? "_s" : ""
|
||||
}`;
|
||||
|
||||
const all: { cookieName: string; token: string; priority: number }[] =
|
||||
[];
|
||||
const all: { cookieName: string; token: string; priority: number }[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(sessions)) {
|
||||
const parts = key.split(".");
|
||||
|
|
|
@ -11,8 +11,7 @@ import { fromError } from "zod-validation-error";
|
|||
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import {
|
||||
verifyResourceAccessToken,
|
||||
verifyResourceAccessTokenSHA256
|
||||
verifyResourceAccessToken
|
||||
} from "@server/auth/verifyResourceAccessToken";
|
||||
import config from "@server/lib/config";
|
||||
import stoi from "@server/lib/stoi";
|
||||
|
@ -98,7 +97,6 @@ export async function authWithAccessToken(
|
|||
}
|
||||
|
||||
const res = await verifyResourceAccessToken({
|
||||
resource: foundResource,
|
||||
accessTokenId,
|
||||
accessToken
|
||||
});
|
||||
|
@ -108,7 +106,7 @@ export async function authWithAccessToken(
|
|||
error = res.error;
|
||||
resource = foundResource;
|
||||
} else {
|
||||
const res = await verifyResourceAccessTokenSHA256({
|
||||
const res = await verifyResourceAccessToken({
|
||||
accessToken
|
||||
});
|
||||
|
||||
|
|
|
@ -110,9 +110,12 @@ export async function traefikConfigProvider(
|
|||
userSessionCookieName:
|
||||
config.getRawConfig().server
|
||||
.session_cookie_name,
|
||||
|
||||
// deprecated
|
||||
accessTokenQueryParam:
|
||||
config.getRawConfig().server
|
||||
.resource_access_token_param,
|
||||
|
||||
resourceSessionRequestParam:
|
||||
config.getRawConfig().server
|
||||
.resource_session_request_param
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import db from "@server/db";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { sql } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
const version = "1.2.0";
|
||||
|
||||
|
@ -19,5 +22,48 @@ export default async function migration() {
|
|||
throw e;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!rawConfig.flags) {
|
||||
rawConfig.flags = {};
|
||||
}
|
||||
|
||||
rawConfig.server.resource_access_token_headers = {
|
||||
id: "P-Access-Token-ID",
|
||||
token: "P-Access-Token"
|
||||
};
|
||||
|
||||
// Write the updated YAML back to the file
|
||||
const updatedYaml = yaml.dump(rawConfig);
|
||||
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||
|
||||
console.log(`Added new config option: resource_access_token_headers`);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config`
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue