Skip to content

Commit a36e795

Browse files
authored
feat: add API-based OAuth endpoints for webapp integration (#328)
Add endpoints for webapp to handle GitHub OAuth without redirecting through core: - GET /api/auth/url - returns GitHub OAuth URL with webapp callback - POST /api/auth/callback - exchanges code for session, returns sessionId This enables the webapp to control the OAuth UX while core handles token exchange and session management.
1 parent 5099986 commit a36e795

File tree

1 file changed

+110
-9
lines changed

1 file changed

+110
-9
lines changed

src/index.js

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,14 @@ async function startServer() {
242242
// ============================================================================
243243
// API Authentication Endpoints
244244
// ============================================================================
245-
// Webapp Integration Pattern:
246-
// 1. Webapp redirects browser to /login?redirect=<webapp-callback-url>
247-
// 2. OAuth flow completes, user redirected back to webapp callback
248-
// 3. Webapp calls GET /api/auth/session to get sessionId
249-
// 4. Webapp sets sessionId as httpOnly cookie
250-
// 5. Subsequent requests: proxy converts cookie to SESSION header
251-
//
252-
// Deprecated endpoints (remove after webapp migration complete):
253-
// - POST /api/auth/logout -> use GET /user/logout or GET /api/user/logout
245+
// Webapp Integration Pattern (API-based):
246+
// 1. Webapp calls GET /api/auth/url?redirect_uri=<callback> to get OAuth URL
247+
// 2. Webapp redirects user to GitHub OAuth URL
248+
// 3. GitHub redirects to webapp callback with ?code=
249+
// 4. Webapp calls POST /api/auth/callback with {code, redirect_uri}
250+
// 5. Core exchanges code for token, creates session, returns {sessionId}
251+
// 6. Webapp sets sessionId as httpOnly cookie
252+
// 7. Subsequent requests: proxy converts cookie to SESSION header
254253
// ============================================================================
255254

256255
app.get('/api/auth/status', async function (req, res) {
@@ -328,6 +327,108 @@ async function startServer() {
328327
});
329328
});
330329

330+
// Returns GitHub OAuth URL for webapp to redirect to
331+
app.get('/api/auth/url', function (req, res) {
332+
const redirectUri = req.query.redirect_uri;
333+
if (!redirectUri) {
334+
return res.status(400).json({ error: 'redirect_uri is required' });
335+
}
336+
337+
const oauthUrl =
338+
`https://github.com/login/oauth/authorize?` +
339+
`client_id=${process.env.GITHUB_CLIENT_ID}` +
340+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
341+
`&scope=public_repo,read:org,admin:repo_hook`;
342+
343+
res.json({ url: oauthUrl });
344+
});
345+
346+
// Exchanges OAuth code for session - webapp calls this after GitHub callback
347+
app.post('/api/auth/callback', express.json(), async function (req, res) {
348+
const { code, redirect_uri } = req.body;
349+
350+
if (!code || !redirect_uri) {
351+
return res
352+
.status(400)
353+
.json({ error: 'code and redirect_uri are required' });
354+
}
355+
356+
try {
357+
// Exchange code for access token
358+
const tokenResponse = await fetch(
359+
'https://github.com/login/oauth/access_token',
360+
{
361+
method: 'POST',
362+
headers: {
363+
Accept: 'application/json',
364+
'Content-Type': 'application/json',
365+
},
366+
body: JSON.stringify({
367+
client_id: process.env.GITHUB_CLIENT_ID,
368+
client_secret: process.env.GITHUB_CLIENT_SECRET,
369+
code,
370+
redirect_uri,
371+
}),
372+
}
373+
);
374+
375+
if (!tokenResponse.ok) {
376+
throw new Error(
377+
`GitHub token exchange failed: ${tokenResponse.status}`
378+
);
379+
}
380+
381+
const tokenData = await tokenResponse.json();
382+
if (tokenData.error || !tokenData.access_token) {
383+
return res.status(401).json({
384+
error: tokenData.error_description || 'Failed to obtain access token',
385+
});
386+
}
387+
388+
// Fetch GitHub user info
389+
const userResponse = await fetch('https://api.github.com/user', {
390+
headers: {
391+
Authorization: `Bearer ${tokenData.access_token}`,
392+
Accept: 'application/vnd.github.v3+json',
393+
},
394+
});
395+
396+
if (!userResponse.ok) {
397+
throw new Error(`Failed to fetch GitHub user: ${userResponse.status}`);
398+
}
399+
400+
const githubUser = await userResponse.json();
401+
if (!githubUser.id) {
402+
return res.status(401).json({ error: 'Invalid GitHub user response' });
403+
}
404+
405+
// Find or create user
406+
let user = await User.findByGithubUserId(githubUser.id);
407+
if (user) {
408+
user = await User.update(user._id, {
409+
githubAccessToken: tokenData.access_token,
410+
});
411+
} else {
412+
user = await User.create({
413+
githubUserId: githubUser.id,
414+
githubAccessToken: tokenData.access_token,
415+
});
416+
}
417+
418+
// Create session
419+
req.session.userId = user._id.toString();
420+
421+
// Return session ID for webapp to set as httpOnly cookie
422+
res.json({
423+
authenticated: true,
424+
sessionId: req.sessionID,
425+
});
426+
} catch (error) {
427+
console.error('OAuth callback error:', error);
428+
res.status(500).json({ error: 'Authentication failed' });
429+
}
430+
});
431+
331432
app.get('/v1/user', async function (req, res) {
332433
if (!req.session.userId) {
333434
return res.status(401).end();

0 commit comments

Comments
 (0)