Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -324,23 +324,34 @@ <h1 class="display-4">Welcome to Startup API</h1>

<!-- Configuration -->
<section class="card">
<div class="card-header">Environment Variables</div>
<div class="card-header">Required Configuration</div>
<div class="card-body">
<p>
Configure your installation by setting these variables in your
<code>wrangler.jsonc</code> or Cloudflare Dashboard:
These variables <strong>must</strong> be set in your <code>wrangler.jsonc</code> or Cloudflare Dashboard for the worker to function.
Until these are configured, the worker serves this documentation page.
</p>

<div class="mb-4">
<h5>
<span class="variable-name">ORIGIN_URL</span>
<span class="badge bg-danger">REQUIRED</span>
</h5>
<p>The base URL of the application you want to proxy, e.g. your app URL.</p>
<p>Until you set this variable the worker serves this help page.</p>
<pre>ORIGIN_URL = "https://your-app-origin.com"</pre>
</div>

<div class="mb-4">
<h5>
<span class="variable-name">SESSION_SECRET</span>
</h5>
<p>A long, random string used to encrypt session cookies. <strong>Keep this secret!</strong></p>
<pre>SESSION_SECRET = "your-long-random-secret-string"</pre>
</div>
</div>
</section>

<section class="card">
<div class="card-header">Optional Configuration</div>
<div class="card-body">
<div class="mb-4">
<h5>
<span class="variable-name">USERS_PATH</span>
Expand Down
56 changes: 56 additions & 0 deletions src/CookieManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export class CookieManager {
private keyPromise: Promise<CryptoKey> | null = null;

constructor(private secret: string) {}

private async getKey(): Promise<CryptoKey> {
if (this.keyPromise) return this.keyPromise;

this.keyPromise = (async () => {
const encoder = new TextEncoder();
const secretData = encoder.encode(this.secret);
const hash = await crypto.subtle.digest('SHA-256', secretData);
return crypto.subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
})();

return this.keyPromise;
}

async encrypt(value: string): Promise<string> {
const key = await this.getKey();
const encoder = new TextEncoder();
const data = encoder.encode(value);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);

const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv);
combined.set(new Uint8Array(ciphertext), iv.length);

return btoa(String.fromCharCode(...combined))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

async decrypt(encrypted: string): Promise<string | null> {
try {
const key = await this.getKey();
const base64 = encrypted.replace(/-/g, '+').replace(/_/g, '/');
const combined = new Uint8Array(
atob(base64)
.split('')
.map((c) => c.charCodeAt(0)),
);

const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);

const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
return new TextDecoder().decode(decrypted);
} catch (e) {
console.error('Failed to decrypt cookie:', e);
return null;
}
}
}
1 change: 1 addition & 0 deletions src/StartupAPIEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export type StartupAPIEnv = {
TWITCH_CLIENT_ID: string;
TWITCH_CLIENT_SECRET: string;
ADMIN_IDS: string;
SESSION_SECRET: string;
SYSTEM: DurableObjectNamespace;
} & Env;
14 changes: 11 additions & 3 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import type { StartupAPIEnv } from '../StartupAPIEnv';
import { GoogleProvider } from './GoogleProvider';
import { TwitchProvider } from './TwitchProvider';
import { OAuthProvider } from './OAuthProvider';

export async function handleAuth(request: Request, env: StartupAPIEnv, url: URL, usersPath: string): Promise<Response> {
import { CookieManager } from '../CookieManager';

export async function handleAuth(
request: Request,
env: StartupAPIEnv,
url: URL,
usersPath: string,
cookieManager: CookieManager,
): Promise<Response> {
const path = url.pathname;
const authPath = usersPath + 'auth';

Expand Down Expand Up @@ -152,8 +159,9 @@ export async function handleAuth(request: Request, env: StartupAPIEnv, url: URL,

// Set cookie and redirect home
const doId = id.toString();
const encryptedSession = await cookieManager.encrypt(`${session.sessionId}:${doId}`);
const headers = new Headers();
headers.set('Set-Cookie', `session_id=${session.sessionId}:${doId}; Path=/; HttpOnly; Secure; SameSite=Lax`);
headers.set('Set-Cookie', `session_id=${encryptedSession}; Path=/; HttpOnly; Secure; SameSite=Lax`);
headers.set('Location', '/');

return new Response(null, { status: 302, headers });
Expand Down
Loading