azure login und cal sync
This commit is contained in:
parent
2f9218ce0e
commit
b6f35431df
|
|
@ -1,21 +1,20 @@
|
|||
AZURE_CLIENT_ID=<app-registration-client-id>
|
||||
# ── SERVER ───────────────────────────────────────────────────────────────────
|
||||
PORT=9000
|
||||
DEV_AUTH_BYPASS=false
|
||||
AI_LOCK_EXPIRY_HOURS=168
|
||||
|
||||
# Azure AD — server app registration (validates incoming JWTs)
|
||||
AZURE_CLIENT_ID=<server-app-registration-client-id>
|
||||
AZURE_TENANT_ID=<azure-ad-tenant-id>
|
||||
|
||||
# Set to true for local dev to skip JWT verification (never use in production)
|
||||
# DEV_AUTH_BYPASS=true
|
||||
# Graph / OBO — required for calendar integration
|
||||
# App Registration → API permissions → Graph → Calendars.Read (delegated) → grant admin consent
|
||||
# App Registration → Certificates & secrets → New client secret
|
||||
AZURE_CLIENT_SECRET=<client-secret-value>
|
||||
# aud of client token must match this. Only needed if frontend uses a different app registration.
|
||||
AZURE_OBO_CLIENT_ID=<frontend-app-registration-client-id>
|
||||
|
||||
# AI lock duration in hours (default: 24, use 168 for 7 days)
|
||||
# AI_LOCK_EXPIRY_HOURS=168
|
||||
|
||||
# Graph API calendar integration (OBO flow)
|
||||
# AZURE_CLIENT_SECRET=<client-secret-value>
|
||||
# AZURE_OBO_CLIENT_ID=<client-id of the app registration the frontend uses>
|
||||
# Only needed if VITE_AZURE_CLIENT_ID != AZURE_CLIENT_ID (separate frontend/backend registrations).
|
||||
# The OBO assertion token's audience must match this client_id.
|
||||
# Required: App Registration → API permissions → Microsoft Graph → Calendars.Read (delegated) → Grant admin consent
|
||||
# Then: Certificates & secrets → New client secret → copy value here
|
||||
|
||||
# Client needs VITE_ prefix — create client/.env with:
|
||||
# VITE_AZURE_CLIENT_ID=<same as above>
|
||||
# VITE_AZURE_TENANT_ID=<same as above>
|
||||
# VITE_DEV_AUTH_BYPASS=true ← DEV ONLY: skips MS login in browser (never set in production)
|
||||
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
|
||||
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>
|
||||
# VITE_AZURE_TENANT_ID=<azure-ad-tenant-id>
|
||||
# VITE_DEV_AUTH_BYPASS=true # DEV ONLY — never set in production
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -21,11 +21,17 @@ const oboClientId = process.env.AZURE_OBO_CLIENT_ID ?? process.env.AZURE_CLIENT_
|
|||
const clientSecret = process.env.AZURE_CLIENT_SECRET ?? '';
|
||||
|
||||
async function getOboToken(userToken: string, userId: string): Promise<string> {
|
||||
console.log(`[graph] getOboToken userId=${userId} tenantId=${tenantId || '(missing)'} oboClientId=${oboClientId || '(missing)'} secretSet=${!!clientSecret}`);
|
||||
const cached = tokenCache.get(userId);
|
||||
if (cached && cached.expiresAt > Date.now() + 60_000) {
|
||||
console.log(`[graph] OBO token from cache, expires in ${Math.round((cached.expiresAt - Date.now()) / 1000)}s`);
|
||||
return cached.accessToken;
|
||||
}
|
||||
|
||||
if (!tenantId || !oboClientId || !clientSecret) {
|
||||
throw new Error(`OBO config incomplete: tenantId=${!!tenantId} oboClientId=${!!oboClientId} secret=${!!clientSecret}`);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
client_id: oboClientId,
|
||||
client_secret: clientSecret,
|
||||
|
|
@ -35,6 +41,7 @@ async function getOboToken(userToken: string, userId: string): Promise<string> {
|
|||
scope: 'https://graph.microsoft.com/Calendars.Read',
|
||||
});
|
||||
|
||||
console.log(`[graph] OBO token exchange → https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`);
|
||||
const res = await fetch(
|
||||
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body },
|
||||
|
|
@ -42,10 +49,12 @@ async function getOboToken(userToken: string, userId: string): Promise<string> {
|
|||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text();
|
||||
console.error(`[graph] OBO exchange failed ${res.status}: ${detail}`);
|
||||
throw new Error(`OBO token exchange failed (${res.status}): ${detail}`);
|
||||
}
|
||||
|
||||
const json = await res.json() as { access_token: string; expires_in: number };
|
||||
console.log(`[graph] OBO token obtained, expires_in=${json.expires_in}s`);
|
||||
const entry: OboTokenEntry = {
|
||||
accessToken: json.access_token,
|
||||
expiresAt: Date.now() + json.expires_in * 1000,
|
||||
|
|
@ -66,6 +75,7 @@ export async function getCalendarEvents(
|
|||
date: string,
|
||||
): Promise<CalendarEvent[]> {
|
||||
|
||||
console.log(`[graph] getCalendarEvents userId=${userId} email=${userEmail} date=${date} tokenLen=${userToken.length}`);
|
||||
const graphToken = await getOboToken(userToken, userId);
|
||||
|
||||
const start = `${date}T00:00:00`;
|
||||
|
|
@ -87,8 +97,10 @@ export async function getCalendarEvents(
|
|||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text();
|
||||
console.error(`[graph] calendarView failed ${res.status}: ${detail}`);
|
||||
throw new Error(`Graph calendarView failed (${res.status}): ${detail}`);
|
||||
}
|
||||
console.log(`[graph] calendarView OK`);
|
||||
|
||||
type GraphEvent = {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ calendar.get('/events', async (c) => {
|
|||
const auth = c.get('auth');
|
||||
const rawToken = c.req.header('Authorization')?.slice(7) ?? '';
|
||||
|
||||
// OBO requires a real MSAL JWT — API keys and dev-bypass tokens don't work
|
||||
const looksLikeJwt = rawToken.startsWith('eyJ') && rawToken.split('.').length === 3;
|
||||
console.log(`[calendar] rawToken prefix="${rawToken.slice(0, 8)}..." looksLikeJwt=${looksLikeJwt} userId=${auth.userId}`);
|
||||
if (!looksLikeJwt) {
|
||||
return c.json({ error: 'graph_unavailable', detail: 'Calendar requires MSAL login (OBO not possible with API key or dev-bypass token)' }, 502);
|
||||
}
|
||||
|
||||
try {
|
||||
const events = await getCalendarEvents(rawToken, auth.userId, auth.email, date);
|
||||
return c.json(events);
|
||||
|
|
|
|||
Loading…
Reference in New Issue