Support Codex OAuth login and add CLI setup docs
This commit is contained in:
60
src/oauth.ts
60
src/oauth.ts
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user