import { NextResponse } from "next/server"; import { loadConfig } from "@/lib/config"; export const dynamic = "force-dynamic"; export const maxDuration = 10; const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const MISSING_CONFIG_ERROR = "Microsoft OAuth is not configured. The built-in Azure CLI client ID can no longer be used because Microsoft treats it as a first-party application and requires admin pre-authorization. " + "To connect a Microsoft 365 mailbox, register your own Azure AD application and set AZURE_AD_CLIENT_ID (or AZURE_CLIENT_ID / MICROSOFT_CLIENT_ID) and AZURE_AD_TENANT_ID (or 'common'). " + "See the README for step-by-step registration instructions."; /** * POST /api/microsoft/devicecode * * Initiates the OAuth device code flow for Microsoft 365 / Outlook. * Uses the Azure AD client ID configured via environment variables or the * app config file. A dedicated app registration is required because the Azure * CLI client ID is a first-party application that Microsoft no longer allows * users to consent to directly. * * Returns: * device_code: string — internal code for polling * user_code: string — code the user enters at the verification URI * verification_uri: string — URL the user visits to login * expires_in: number — seconds until the code expires * interval: number — polling interval in seconds * message: string — human-readable instructions */ export async function POST() { let config; try { config = loadConfig(); } catch (e) { console.error("[microsoft/devicecode] loadConfig error:", e); config = { graph: {} }; } const CLIENT_ID = process.env.AZURE_AD_CLIENT_ID || process.env.AZURE_CLIENT_ID || process.env.MICROSOFT_CLIENT_ID || config.graph?.clientId || ""; const TENANT = process.env.AZURE_AD_TENANT_ID || process.env.AZURE_TENANT_ID || process.env.MICROSOFT_TENANT_ID || config.graph?.tenantId || "common"; if (!CLIENT_ID || !UUID_REGEX.test(CLIENT_ID)) { return NextResponse.json({ error: MISSING_CONFIG_ERROR }, { status: 400 }); } const SCOPES = [ "https://graph.microsoft.com/Mail.Read", "https://graph.microsoft.com/Mail.ReadWrite", "https://graph.microsoft.com/Files.Read", "https://graph.microsoft.com/Files.Read.All", "offline_access", "openid", "profile", ]; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 9000); try { const res = await fetch( `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPES.join(" "), }), signal: controller.signal, } ); const text = await res.text(); const data = text ? JSON.parse(text) : {}; if (!res.ok) { return NextResponse.json( { error: data.error_description || data.error || "Failed to start device code flow" }, { status: 500 } ); } return NextResponse.json({ device_code: data.device_code, user_code: data.user_code, verification_uri: data.verification_uri, expires_in: data.expires_in, interval: data.interval || 5, message: data.message || `Go to ${data.verification_uri} and enter code ${data.user_code}`, client_id: CLIENT_ID, tenant_id: TENANT, scopes: SCOPES, }); } finally { clearTimeout(timeout); } } catch (e: any) { return NextResponse.json({ error: e.name === "AbortError" ? "Device code request timed out" : e.message }, { status: 504 }); } }