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

@@ -165,6 +165,28 @@ function extractBearerToken(req: express.Request): string | undefined {
return undefined;
}
function extractBasicClientCredentials(req: express.Request): { clientId?: string; clientSecret?: string } {
const authHeader = req.headers.authorization as string | undefined;
if (!authHeader || !authHeader.startsWith('Basic ')) {
return {};
}
try {
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
const separatorIndex = decoded.indexOf(':');
if (separatorIndex === -1) {
return {};
}
return {
clientId: decoded.slice(0, separatorIndex),
clientSecret: decoded.slice(separatorIndex + 1),
};
} catch {
return {};
}
}
async function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
try {
// No API key configured = open access
@@ -236,6 +258,8 @@ app.get('/oauth/authorize', async (req, res) => {
const redirectUri = req.query.redirect_uri as string | undefined;
const state = req.query.state as string | undefined;
const scope = req.query.scope as string | undefined;
const codeChallenge = req.query.code_challenge as string | undefined;
const codeChallengeMethod = req.query.code_challenge_method as string | undefined;
const responseType = req.query.response_type as string | undefined;
if (!clientId || !redirectUri) {
@@ -254,7 +278,14 @@ app.get('/oauth/authorize', async (req, res) => {
}
res.setHeader('Content-Type', 'text/html');
res.send(getAuthorizeHtml({ client_id: clientId, redirect_uri: redirectUri, state, scope }));
res.send(getAuthorizeHtml({
client_id: clientId,
redirect_uri: redirectUri,
state,
scope,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
}));
});
app.post('/oauth/authorize', async (req, res) => {
@@ -262,6 +293,8 @@ app.post('/oauth/authorize', async (req, res) => {
const redirectUri = req.body.redirect_uri as string | undefined;
const state = req.body.state as string | undefined;
const scope = req.body.scope as string | undefined;
const codeChallenge = req.body.code_challenge as string | undefined;
const codeChallengeMethod = req.body.code_challenge_method as string | undefined;
const action = req.body.action as string | undefined;
if (!clientId || !redirectUri) {
@@ -284,7 +317,7 @@ app.post('/oauth/authorize', async (req, res) => {
return;
}
const code = await createAuthCode(clientId, redirectUri, scope);
const code = await createAuthCode(clientId, redirectUri, scope, codeChallenge, codeChallengeMethod);
const url = new URL(redirectUri);
url.searchParams.set('code', code.code);
if (state) url.searchParams.set('state', state);
@@ -299,17 +332,19 @@ app.post('/oauth/token', async (req, res) => {
return;
}
const clientId = req.body.client_id as string | undefined;
const clientSecret = req.body.client_secret as string | undefined;
const basicAuth = extractBasicClientCredentials(req);
const clientId = (req.body.client_id as string | undefined) || basicAuth.clientId;
const clientSecret = (req.body.client_secret as string | undefined) || basicAuth.clientSecret;
const code = req.body.code as string | undefined;
const redirectUri = req.body.redirect_uri as string | undefined;
const codeVerifier = req.body.code_verifier as string | undefined;
if (!clientId || !clientSecret || !code || !redirectUri) {
if (!clientId || !code || !redirectUri || (!clientSecret && !codeVerifier)) {
res.status(400).json({ error: 'invalid_request' });
return;
}
const token = await exchangeCodeForToken(clientId, clientSecret, code, redirectUri);
const token = await exchangeCodeForToken(clientId, clientSecret, code, redirectUri, codeVerifier);
if (!token) {
res.status(400).json({ error: 'invalid_grant' });
return;
@@ -973,7 +1008,8 @@ const oauthDiscovery = {
registration_endpoint: `${SERVER_URL}/oauth/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code'],
token_endpoint_auth_methods_supported: ['client_secret_post'],
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
code_challenge_methods_supported: ['S256', 'plain'],
};
const protectedResourceMetadata = {