From fd043c9842afd8ee71adcc4136329d7b0b784b70 Mon Sep 17 00:00:00 2001 From: Benjamin Oddou Date: Sun, 16 Nov 2025 21:44:03 +0100 Subject: [PATCH 1/2] feat(auth): add support for non-interactive GitHub PAT login --- src/module/src/module.ts | 6 ++ src/module/src/runtime/server/routes/admin.ts | 14 ++- .../runtime/server/routes/auth/github.get.ts | 95 ++++++++++++++++++- 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/module/src/module.ts b/src/module/src/module.ts index 794bcf7b..9eabe278 100644 --- a/src/module/src/module.ts +++ b/src/module/src/module.ts @@ -33,6 +33,11 @@ interface ModuleOptions { * @default process.env.STUDIO_GITHUB_CLIENT_SECRET */ clientSecret?: string + /** + * A GitHub Personal Access Token (PAT) to bypass OAuth. + * @default process.env.STUDIO_GITHUB_PAT + */ + pat?: string } } /** @@ -116,6 +121,7 @@ export default defineNuxtModule({ github: { clientId: process.env.STUDIO_GITHUB_CLIENT_ID, clientSecret: process.env.STUDIO_GITHUB_CLIENT_SECRET, + pat: process.env.STUDIO_GITHUB_PAT, }, }, i18n: { diff --git a/src/module/src/runtime/server/routes/admin.ts b/src/module/src/runtime/server/routes/admin.ts index f6b7cd5d..8d8c3869 100644 --- a/src/module/src/runtime/server/routes/admin.ts +++ b/src/module/src/runtime/server/routes/admin.ts @@ -1,6 +1,14 @@ -import { eventHandler, getQuery, sendRedirect, setCookie } from 'h3' - -export default eventHandler((event) => { +import { eventHandler, getQuery, sendRedirect, setCookie, useSession } from 'h3' +import { useRuntimeConfig } from '#imports' + +export default eventHandler(async (event) => { + const session = await useSession(event, { + name: 'studio-session', + password: useRuntimeConfig(event).studio?.auth?.sessionSecret, + }) + if (session.data?.user) { + return sendRedirect(event, '/') + } const { redirect } = getQuery(event) if (redirect) { setCookie(event, 'studio-redirect', String(redirect), { diff --git a/src/module/src/runtime/server/routes/auth/github.get.ts b/src/module/src/runtime/server/routes/auth/github.get.ts index 24ed6a09..e4513f2f 100644 --- a/src/module/src/runtime/server/routes/auth/github.get.ts +++ b/src/module/src/runtime/server/routes/auth/github.get.ts @@ -18,6 +18,11 @@ export interface OAuthGitHubConfig { * @default process.env.STUDIO_GITHUB_CLIENT_SECRET */ clientSecret?: string + /** + * A GitHub Personal Access Token (PAT) to bypass OAuth. + * @default process.env.STUDIO_GITHUB_PAT + */ + pat?: string /** * GitHub OAuth Scope * @default [] @@ -84,6 +89,7 @@ export default eventHandler(async (event: H3Event) => { const config = defu(studioConfig?.auth?.github, { clientId: process.env.STUDIO_GITHUB_CLIENT_ID, clientSecret: process.env.STUDIO_GITHUB_CLIENT_SECRET, + pat: process.env.STUDIO_GITHUB_PAT, redirectURL: process.env.STUDIO_GITHUB_REDIRECT_URL, authorizationURL: 'https://github.com/login/oauth/authorize', tokenURL: 'https://github.com/login/oauth/access_token', @@ -102,10 +108,91 @@ export default eventHandler(async (event: H3Event) => { }) } - if (!config.clientId || !config.clientSecret) { + // Check auth strategies + const hasOAuth = config.clientId && config.clientSecret + const hasPat = !!config.pat + + if (hasPat && hasOAuth) { + console.warn('Nuxt Studio: Both PAT and OAuth (clientId/clientSecret) are configured. Defaulting to OAuth flow.') + } + + // PAT-only flow + // If *only* PAT is provided (no OAuth), log in directly. + if (hasPat && !hasOAuth) { + const accessToken = config.pat! + let user: Endpoints['GET /user']['response']['data'] + try { + user = await $fetch(`${config.apiURL}/user`, { + headers: { + 'User-Agent': `Nuxt-Studio-PAT-Sync`, + 'Authorization': `token ${accessToken}`, + }, + }) + } + catch (e) { + throw createError({ statusCode: 500, message: 'Failed to fetch user with GitHub PAT', data: e }) + } + + // if no public email, check the private ones + if (!user.email && config.emailRequired) { + try { + const emails: Endpoints['GET /user/emails']['response']['data'] = await $fetch(`${config.apiURL}/user/emails`, { + headers: { + 'User-Agent': `Nuxt-Studio-PAT-Sync`, + 'Authorization': `token ${accessToken}`, + }, + }) + const primaryEmail = emails.find((email: { primary: boolean }) => email.primary) + if (primaryEmail) { + user.email = primaryEmail.email + } + else { + console.warn('Nuxt Studio: Could not find primary email for PAT user.') + } + } + catch (e) { + console.error('Nuxt Studio: Failed to fetch emails for PAT user.', e) + } + } + + // Success: Create session + const session = await useSession(event, { + name: 'studio-session', + password: useRuntimeConfig(event).studio?.auth?.sessionSecret, + }) + + await session.update(defu({ + user: { + contentUser: true, + githubId: user.id, + githubToken: accessToken, + name: user.name ?? user.login, + avatar: user.avatar_url ?? '', + email: user.email ?? '', + provider: 'github-pat', + }, + }, session.data)) + + const redirect = decodeURIComponent(getCookie(event, 'studio-redirect') || '') + deleteCookie(event, 'studio-redirect') + + // Set a cookie to indicate that the session is active + setCookie(event, 'studio-session-check', 'true', { httpOnly: false }) + + // make sure the redirect is a valid relative path (avoid also // which is not a valid URL) + if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) { + return sendRedirect(event, redirect) + } + + return sendRedirect(event, '/') + } + + // If we're here, it means PAT-only flow didn't run. + // We must now be in OAuth flow, so check for OAuth creds. + if (!hasOAuth) { throw createError({ statusCode: 500, - message: 'Missing GitHub client ID or secret', + message: 'Missing GitHub client ID/secret OR GitHub PAT', data: config, }) } @@ -151,8 +238,8 @@ export default eventHandler(async (event: H3Event) => { const token = await requestAccessToken(config.tokenURL as string, { body: { grant_type: 'authorization_code', - client_id: config.clientId, - client_secret: config.clientSecret, + client_id: config.clientId ?? '', + client_secret: config.clientSecret ?? '', redirect_uri: config.redirectURL, code: query.code, }, From 8d9efc6b349db63df3f0aca50e8ee207bbdb1eab Mon Sep 17 00:00:00 2001 From: Benjamin Oddou Date: Mon, 17 Nov 2025 09:15:25 +0100 Subject: [PATCH 2/2] fix(security): prevent predictable session secret in PAT-only auth --- src/module/src/module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/module/src/module.ts b/src/module/src/module.ts index 9eabe278..6895d00b 100644 --- a/src/module/src/module.ts +++ b/src/module/src/module.ts @@ -166,6 +166,7 @@ export default defineNuxtModule({ sessionSecret: createHash('md5').update([ options.auth?.github?.clientId, options.auth?.github?.clientSecret, + options.auth?.github?.pat, ].join('')).digest('hex'), // @ts-expect-error todo fix github type issue github: options.auth?.github,