File size: 3,873 Bytes
9646e24
3f13033
9646e24
 
 
 
3f13033
 
 
 
 
 
 
9646e24
 
 
 
3f13033
 
 
 
9646e24
 
 
 
 
 
 
 
 
 
3f13033
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9646e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f13033
9646e24
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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 });
  }
}