Support Codex OAuth login and add CLI setup docs

This commit is contained in:
Garfield
2026-05-11 10:39:24 -04:00
parent ffb67560b9
commit 6bf4cfd069
6 changed files with 458 additions and 113 deletions

View File

@@ -18,6 +18,8 @@ interface AuthCode {
client_id: string;
redirect_uri: string;
scope?: string;
code_challenge?: string;
code_challenge_method?: string;
expires_at: number;
}
@@ -57,6 +59,19 @@ export function generateAccessToken(): string {
return crypto.randomBytes(32).toString('hex');
}
function verifyPkce(codeVerifier: string, storedChallenge: string, method?: string): boolean {
if (!method || method === 'plain') {
return codeVerifier === storedChallenge;
}
if (method === 'S256') {
const hashed = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
return hashed === storedChallenge;
}
return false;
}
export async function registerClient(body: {
client_name?: string;
redirect_uris?: string[];
@@ -122,20 +137,24 @@ export async function getClient(clientId: string): Promise<Client | undefined> {
export async function createAuthCode(
clientId: string,
redirectUri: string,
scope?: string
scope?: string,
codeChallenge?: string,
codeChallengeMethod?: string
): Promise<AuthCode> {
const code: AuthCode = {
code: generateAuthCode(),
client_id: clientId,
redirect_uri: redirectUri,
scope,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
expires_at: Date.now() + AUTH_CODE_EXPIRY_MS,
};
const pool = getPool();
await pool.execute(
'INSERT INTO oauth_auth_codes (code, client_id, redirect_uri, expires_at) VALUES (?, ?, ?, ?)',
[code.code, clientId, redirectUri, new Date(code.expires_at)]
'INSERT INTO oauth_auth_codes (code, client_id, redirect_uri, scope, code_challenge, code_challenge_method, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[code.code, clientId, redirectUri, scope || null, codeChallenge || null, codeChallengeMethod || null, new Date(code.expires_at)]
);
console.log(`[oauth] Created auth code ${code.code.slice(0, 8)}... for client ${clientId}`);
@@ -144,9 +163,10 @@ export async function createAuthCode(
export async function exchangeCodeForToken(
clientId: string,
clientSecret: string,
clientSecret: string | undefined,
code: string,
redirectUri: string
redirectUri: string,
codeVerifier?: string
): Promise<Token | null> {
let client: Client | undefined;
try {
@@ -156,7 +176,17 @@ export async function exchangeCodeForToken(
return null;
}
if (!client || client.client_secret !== clientSecret) {
if (!client) {
console.log('[oauth] Invalid client');
return null;
}
if (clientSecret) {
if (client.client_secret !== clientSecret) {
console.log('[oauth] Invalid client credentials');
return null;
}
} else if (!codeVerifier) {
console.log('[oauth] Invalid client credentials');
return null;
}
@@ -180,6 +210,18 @@ export async function exchangeCodeForToken(
return null;
}
if (authCode.code_challenge) {
if (!codeVerifier) {
console.log('[oauth] Missing code_verifier for PKCE exchange');
return null;
}
if (!verifyPkce(codeVerifier, authCode.code_challenge, authCode.code_challenge_method || undefined)) {
console.log('[oauth] PKCE verification failed');
return null;
}
}
// Mark auth code as used
await db.execute('UPDATE oauth_auth_codes SET used = TRUE WHERE code = ?', [code]);
@@ -230,8 +272,10 @@ export function getAuthorizeHtml(params: {
redirect_uri: string;
state?: string;
scope?: string;
code_challenge?: string;
code_challenge_method?: string;
}): string {
const { client_id, redirect_uri, state, scope } = params;
const { client_id, redirect_uri, state, scope, code_challenge, code_challenge_method } = params;
return `<!DOCTYPE html>
<html lang="en">
<head>
@@ -261,6 +305,8 @@ export function getAuthorizeHtml(params: {
<input type="hidden" name="redirect_uri" value="${escapeHtml(redirect_uri)}">
${state ? `<input type="hidden" name="state" value="${escapeHtml(state)}">` : ''}
${scope ? `<input type="hidden" name="scope" value="${escapeHtml(scope)}">` : ''}
${code_challenge ? `<input type="hidden" name="code_challenge" value="${escapeHtml(code_challenge)}">` : ''}
${code_challenge_method ? `<input type="hidden" name="code_challenge_method" value="${escapeHtml(code_challenge_method)}">` : ''}
<div class="buttons">
<button type="submit" name="action" value="deny" class="deny">Deny</button>
<button type="submit" name="action" value="allow" class="allow">Allow</button>