зеркало из
1
0
Форкнуть 0
This commit is contained in:
Aniello Scotto Di Marco 2024-05-06 23:21:25 -07:00
Родитель 75211b2ed4
Коммит 23140c4c45
8 изменённых файлов: 305 добавлений и 82 удалений

Просмотреть файл

@ -49,3 +49,70 @@ export function extractPostLoginRedirectUri(protocol?: string, host?: string, pa
return undefined;
}
const IV_LENGTH = 16; // For AES, this is always 16
const CIPHER_ALGORITHM = "aes-256-cbc";
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || crypto.randomBytes(16).toString("hex");
const SIGNING_KEY = process.env.SIGNING_KEY || crypto.randomBytes(16).toString("hex");
const bitLength = SIGNING_KEY.length * 8;
const HMAC_ALGORITHM = bitLength <= 256 ? "sha256" : bitLength <= 384 ? "sha384" : "sha512";
export function encryptAndSign(value: string): string | undefined {
try {
// encrypt
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(CIPHER_ALGORITHM, Buffer.from(ENCRYPTION_KEY), iv);
let encrypted = cipher.update(value);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const encryptedValue = iv.toString("hex") + ":" + encrypted.toString("hex");
// sign
const hash = crypto.createHmac(HMAC_ALGORITHM, process.env.SALT || "");
hash.update(encryptedValue);
const signature = hash.digest("hex");
const signedEncryptedValue = signature + ":" + encryptedValue;
return signedEncryptedValue;
} catch {
return undefined;
}
}
export function validateSignatureAndDecrypt(data: string): string | undefined {
try {
const dataSegments: string[] = data.includes(":") ? data.split(":") : [];
if (dataSegments.length < 3) {
return undefined;
}
// validate signature
const signature = dataSegments.shift() || "";
const signedData = dataSegments.join(":");
const hash = crypto.createHmac(HMAC_ALGORITHM, process.env.SALT || "");
hash.update(signedData);
const testSignature = hash.digest("hex");
if (signature !== testSignature) {
return undefined;
}
// decrypt
const iv = Buffer.from(dataSegments.shift() || "", "hex");
const encryptedText = Buffer.from(dataSegments.join(":"), "hex");
const decipher = crypto.createDecipheriv(CIPHER_ALGORITHM, Buffer.from(ENCRYPTION_KEY), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
} catch {
return undefined;
}
}
export function isValueEncryptedAndSigned(value: string) {
const segments = value.split(":");
return segments.length === 3 && segments[0].length === 64 && segments[1].length >= 32;
}

Просмотреть файл

@ -1,22 +1,9 @@
import chalk from "chalk";
import cookie from "cookie";
import { SWA_AUTH_CONTEXT_COOKIE, SWA_AUTH_COOKIE } from "../constants";
import { isValueEncryptedAndSigned, validateSignatureAndDecrypt } from "./auth";
import { logger } from "./logger";
/**
* Check if the StaticWebAppsAuthCookie is available.
* @param cookieValue The cookie value.
* @returns True if StaticWebAppsAuthCookie is found. False otherwise.
*/
export function validateCookie(cookieValue: string | number | string[]) {
if (typeof cookieValue !== "string") {
throw Error(`TypeError: cookie value must be a string`);
}
const cookies = cookie.parse(cookieValue);
return !!cookies[SWA_AUTH_COOKIE];
}
/**
* Serialize a cookie name-value pair into a string that can be used in Set-Cookie header.
* @param cookieName The name for the cookie.
@ -29,21 +16,23 @@ export function serializeCookie(cookieName: string, cookieValue: string, options
return cookie.serialize(cookieName, cookieValue, options);
}
/**
* Check if the StaticWebAppsAuthCookie is available.
* @param cookieValue The cookie value.
* @returns True if StaticWebAppsAuthCookie is found. False otherwise.
*/
export function validateCookie(cookieValue: string | number | string[]) {
return validateCookieByName(SWA_AUTH_COOKIE, cookieValue);
}
/**
*
* @param cookieValue
* @returns A ClientPrincipal object.
*/
export function decodeCookie(cookieValue: string): ClientPrincipal | null {
logger.silly(`decoding StaticWebAppsAuthCookie cookie`);
const cookies = cookie.parse(cookieValue);
if (cookies[SWA_AUTH_COOKIE]) {
const decodedValue = Buffer.from(cookies[SWA_AUTH_COOKIE], "base64").toString();
logger.silly(` - StaticWebAppsAuthCookie: ${chalk.yellow(decodedValue)}`);
return JSON.parse(decodedValue);
}
logger.silly(` - no cookie 'StaticWebAppsAuthCookie' found`);
return null;
const stringValue = decodeCookieByName(SWA_AUTH_COOKIE, cookieValue);
return stringValue ? JSON.parse(stringValue) : null;
}
/**
@ -52,12 +41,7 @@ export function decodeCookie(cookieValue: string): ClientPrincipal | null {
* @returns True if StaticWebAppsAuthContextCookie is found. False otherwise.
*/
export function validateAuthContextCookie(cookieValue: string | number | string[]) {
if (typeof cookieValue !== "string") {
throw Error(`TypeError: cookie value must be a string`);
}
const cookies = cookie.parse(cookieValue);
return !!cookies[SWA_AUTH_CONTEXT_COOKIE];
return validateCookieByName(SWA_AUTH_CONTEXT_COOKIE, cookieValue);
}
/**
@ -66,13 +50,176 @@ export function validateAuthContextCookie(cookieValue: string | number | string[
* @returns StaticWebAppsAuthContextCookie string.
*/
export function decodeAuthContextCookie(cookieValue: string): AuthContext | null {
logger.silly(`decoding StaticWebAppsAuthContextCookie cookie`);
const cookies = cookie.parse(cookieValue);
if (cookies[SWA_AUTH_CONTEXT_COOKIE]) {
const decodedValue = Buffer.from(cookies[SWA_AUTH_CONTEXT_COOKIE], "base64").toString();
logger.silly(` - StaticWebAppsAuthContextCookie: ${chalk.yellow(decodedValue)}`);
return JSON.parse(decodedValue);
const stringValue = decodeCookieByName(SWA_AUTH_CONTEXT_COOKIE, cookieValue);
return stringValue ? JSON.parse(stringValue) : null;
}
// local functions
function getCookie(cookieName: string, cookies: Record<string, string>) {
const nonChunkedCookie = cookies[cookieName];
if (nonChunkedCookie) {
// prefer the non-chunked cookie if it exists
return nonChunkedCookie;
}
logger.silly(` - no cookie 'StaticWebAppsAuthContextCookie' found`);
let chunkedCookie = "";
let chunk = "";
let index = 0;
do {
chunkedCookie = `${chunkedCookie}${chunk}`;
chunk = cookies[`${cookieName}_${index}`];
index += 1;
} while (chunk);
return chunkedCookie;
}
function validateCookieByName(cookieName: string, cookieValue: string | number | string[]) {
if (typeof cookieValue !== "string") {
throw Error(`TypeError: cookie value must be a string`);
}
const cookies = cookie.parse(cookieValue);
return !!getCookie(cookieName, cookies);
}
function decodeCookieByName(cookieName: string, cookieValue: string) {
logger.silly(`decoding ${cookieName} cookie`);
const cookies = cookie.parse(cookieValue);
const value = getCookie(cookieName, cookies);
if (value) {
const decodedValue = Buffer.from(value, "base64").toString();
logger.silly(` - ${cookieName} decoded: ${chalk.yellow(decodedValue)}`);
if (!decodedValue) {
logger.silly(` - failed to decode '${cookieName}'`);
return null;
}
if (isValueEncryptedAndSigned(decodedValue)) {
const decryptedValue = validateSignatureAndDecrypt(decodedValue);
logger.silly(` - ${cookieName} decrypted: ${chalk.yellow(decryptedValue)}`);
if (!decryptedValue) {
logger.silly(` - failed to validate and decrypt '${cookieName}'`);
return null;
}
return decryptedValue;
}
return decodedValue;
}
logger.silly(` - no cookie '${cookieName}' found`);
return null;
}
export interface CookieOptions extends Omit<cookie.CookieSerializeOptions, "expires"> {
name: string;
value: string;
expires?: string;
}
export class CookiesManager {
private readonly _chunkSize = 2000;
private readonly _existingCookies: Record<string, string>;
private _cookiesToSet: Record<string, CookieOptions> = {};
private _cookiesToDelete: Record<string, string> = {};
constructor(requestCookie?: string) {
this._existingCookies = requestCookie ? cookie.parse(requestCookie) : {};
}
private _generateDeleteChunks(name: string, force: boolean /* add the delete cookie even if the corresponding cookie doesn't exist */) {
const cookies: Record<string, CookieOptions> = {};
// check for unchunked cookie
if (force || this._existingCookies[name]) {
cookies[name] = {
name: name,
value: "deleted",
path: "/",
httpOnly: false,
expires: new Date(1).toUTCString(),
};
}
// check for chunked cookie
let found = true;
let index = 0;
while (found) {
const chunkName = `${name}_${index}`;
found = !!this._existingCookies[chunkName];
if (found) {
cookies[chunkName] = {
name: chunkName,
value: "deleted",
path: "/",
httpOnly: false,
expires: new Date(1).toUTCString(),
};
}
index += 1;
}
return cookies;
}
private _generateChunks(options: CookieOptions): CookieOptions[] {
const { name, value } = options;
// pre-populate with cookies for deleting existing chunks
const cookies: Record<string, CookieOptions> = this._generateDeleteChunks(options.name, false);
// generate chunks
if (value !== "deleted") {
const chunkCount = Math.ceil(value.length / this._chunkSize);
let index = 0;
let chunkName = "";
while (index < chunkCount) {
const position = index * this._chunkSize;
const chunk = value.substring(position, position + this._chunkSize);
chunkName = `${name}_${index}`;
cookies[chunkName] = {
...options,
name: chunkName,
value: chunk,
};
index += 1;
}
}
return Object.values(cookies);
}
public addCookieToSet(options: CookieOptions): void {
this._cookiesToSet[options.name.toLowerCase()] = options;
}
public addCookieToDelete(name: string): void {
this._cookiesToDelete[name.toLowerCase()] = name;
}
public getCookies(): CookieOptions[] {
const allCookies: CookieOptions[] = [];
Object.values(this._cookiesToDelete).forEach((cookieName) => {
const chunks = this._generateDeleteChunks(cookieName, true);
allCookies.push(...Object.values(chunks));
});
Object.values(this._cookiesToSet).forEach((cookie) => {
const chunks = this._generateChunks(cookie);
allCookies.push(...chunks);
});
return allCookies;
}
}

Просмотреть файл

@ -1,10 +1,10 @@
import { decodeAuthContextCookie, parseUrl, response, validateAuthContextCookie } from "../../../core";
import { CookiesManager, decodeAuthContextCookie, parseUrl, response, validateAuthContextCookie } from "../../../core";
import * as http from "http";
import * as https from "https";
import * as querystring from "querystring";
import { SWA_CLI_API_URI, SWA_CLI_APP_PROTOCOL } from "../../../core/constants";
import { DEFAULT_CONFIG } from "../../../config";
import { hashStateGuid, isNonceExpired } from "../../../core/utils/auth";
import { encryptAndSign, hashStateGuid, isNonceExpired } from "../../../core/utils/auth";
const getGithubAuthToken = function (codeValue: string, clientId: string, clientSecret: string) {
const data = querystring.stringify({
@ -469,27 +469,27 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess
} catch {}
}
const authCookieString = clientPrincipal && JSON.stringify(clientPrincipal);
const authCookieEncrypted = authCookieString && encryptAndSign(authCookieString);
const authCookie = authCookieEncrypted ? btoa(authCookieEncrypted) : undefined;
const cookiesManager = new CookiesManager(request.headers.cookie);
cookiesManager.addCookieToDelete("StaticWebAppsAuthContextCookie");
if (authCookie) {
cookiesManager.addCookieToSet({
name: "StaticWebAppsAuthCookie",
value: authCookie,
domain: DEFAULT_CONFIG.host,
path: "/",
secure: true,
httpOnly: true,
expires: new Date(Date.now() + 1000 * 60 * 60 * 8).toUTCString(),
});
}
context.res = response({
context,
cookies: [
{
name: "StaticWebAppsAuthContextCookie",
value: "deleted",
path: "/",
secure: true,
httpOnly: true,
expires: new Date(1).toUTCString(),
},
{
name: "StaticWebAppsAuthCookie",
value: clientPrincipal === null ? "deleted" : btoa(JSON.stringify(clientPrincipal)),
domain: DEFAULT_CONFIG.host,
path: "/",
secure: true,
httpOnly: true,
expires: clientPrincipal === null ? new Date(1).toUTCString() : new Date(Date.now() + 1000 * 60 * 60 * 8).toUTCString(),
},
],
cookies: cookiesManager.getCookies(),
status: 302,
headers: {
status: 302,

Просмотреть файл

@ -1,8 +1,8 @@
import { response } from "../../../core";
import { CookiesManager, response } from "../../../core";
import * as http from "http";
import { SWA_CLI_APP_PROTOCOL } from "../../../core/constants";
import { DEFAULT_CONFIG } from "../../../config";
import { extractPostLoginRedirectUri, hashStateGuid, newNonceWithExpiration } from "../../../core/utils/auth";
import { encryptAndSign, extractPostLoginRedirectUri, hashStateGuid, newNonceWithExpiration } from "../../../core/utils/auth";
const httpTrigger = async function (context: Context, request: http.IncomingMessage, customAuth?: SWAConfigFileAuth) {
await Promise.resolve();
@ -50,6 +50,10 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess
postLoginRedirectUri: extractPostLoginRedirectUri(SWA_CLI_APP_PROTOCOL, request.headers.host, request.url),
};
const authContextCookieString = JSON.stringify(authContext);
const authContextCookieEncrypted = encryptAndSign(authContextCookieString);
const authContextCookie = authContextCookieEncrypted ? btoa(authContextCookieEncrypted) : undefined;
const hashedState = hashStateGuid(state);
const redirectUri = `${SWA_CLI_APP_PROTOCOL}://${DEFAULT_CONFIG.host}:${DEFAULT_CONFIG.port}`;
@ -58,18 +62,23 @@ const httpTrigger = async function (context: Context, request: http.IncomingMess
? `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/google/callback&scope=openid+profile+email&state=${hashedState}`
: `https://github.com/login/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}/.auth/login/github/callback&scope=read:user&state=${hashedState}`;
const cookiesManager = new CookiesManager(request.headers.cookie);
if (!authContextCookie) {
cookiesManager.addCookieToDelete("StaticWebAppsAuthContextCookie");
} else {
cookiesManager.addCookieToSet({
name: "StaticWebAppsAuthContextCookie",
value: authContextCookie,
domain: DEFAULT_CONFIG.host,
path: "/",
secure: true,
httpOnly: true,
});
}
context.res = response({
context,
cookies: [
{
name: "StaticWebAppsAuthContextCookie",
value: btoa(JSON.stringify(authContext)),
domain: DEFAULT_CONFIG.host,
path: "/",
secure: true,
httpOnly: true,
},
],
cookies: cookiesManager.getCookies(),
status: 302,
headers: {
status: 302,

Просмотреть файл

@ -1,11 +1,15 @@
import { IncomingMessage } from "http";
import { response } from "../../../core";
import { CookiesManager, response } from "../../../core";
const fs = require("fs").promises;
const path = require("path");
const httpTrigger = async function (context: Context, request: IncomingMessage) {
const body = await fs.readFile(path.join(__dirname, "..", "..", "..", "public", "auth.html"), "utf-8");
const cookiesManager = new CookiesManager(request.headers.cookie);
cookiesManager.addCookieToDelete("StaticWebAppsAuthContextCookie");
context.res = response({
context,
status: 200,

Просмотреть файл

@ -14,7 +14,7 @@ describe("auth-logout-https", () => {
name: "StaticWebAppsAuthCookie",
value: "deleted",
path: "/",
HttpOnly: false,
httpOnly: false,
expires: new Date(1).toUTCString(),
};

Просмотреть файл

@ -14,7 +14,7 @@ describe("auth_logout", () => {
name: "StaticWebAppsAuthCookie",
value: "deleted",
path: "/",
HttpOnly: false,
httpOnly: false,
expires: new Date(1).toUTCString(),
};

Просмотреть файл

@ -1,5 +1,6 @@
import type http from "http";
import { response } from "../../../core";
import { CookiesManager, response } from "../../../core";
// import { response } from "../../../core";
import { SWA_CLI_APP_PROTOCOL } from "../../../core/constants";
export default async function (context: Context, req: http.IncomingMessage) {
@ -17,18 +18,13 @@ export default async function (context: Context, req: http.IncomingMessage) {
const query = new URL(req?.url || "", uri).searchParams;
const location = `${uri}${query.get("post_logout_redirect_uri") || "/"}`;
const cookiesManager = new CookiesManager(req.headers.cookie);
cookiesManager.addCookieToDelete("StaticWebAppsAuthCookie");
context.res = response({
context,
status: 302,
cookies: [
{
name: "StaticWebAppsAuthCookie",
value: "deleted",
path: "/",
HttpOnly: false,
expires: new Date(1).toUTCString(),
},
],
cookies: cookiesManager.getCookies(),
headers: {
Location: location,
},