From b6f35431df8ef338a4f38bbff6b7bd9fbf6cdf16 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Fri, 27 Feb 2026 16:30:32 +0100 Subject: [PATCH] azure login und cal sync --- ka-note/.env.example | 35 ++++++++++++------------ ka-note/server/ka-note.db-shm | Bin 32768 -> 32768 bytes ka-note/server/ka-note.db-wal | Bin 0 -> 111272 bytes ka-note/server/src/lib/graph-service.ts | 12 ++++++++ ka-note/server/src/routes/calendar.ts | 7 +++++ 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/ka-note/.env.example b/ka-note/.env.example index 71f74bb..d1f2754 100644 --- a/ka-note/.env.example +++ b/ka-note/.env.example @@ -1,21 +1,20 @@ -AZURE_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= AZURE_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= +# aud of client token must match this. Only needed if frontend uses a different app registration. +AZURE_OBO_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= -# AZURE_OBO_CLIENT_ID= -# 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= -# VITE_AZURE_TENANT_ID= -# 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= +# VITE_AZURE_TENANT_ID= +# VITE_DEV_AUTH_BYPASS=true # DEV ONLY — never set in production diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..d20222225687299d59afacd4b11aef41f9db3843 100644 GIT binary patch literal 32768 zcmeI*OG*Pl6oug%P1N{|8nu1I7#|2?HW6?GZo!c|5FCjvK_OrUqDyd?NyZKwIu#td z2R$X-ASBS;gNUj6FC4n5z7&1G1yn^JO>;G-6wE67dOj)L?%V6}$^Coh^Zeqp(QY-L zUoH>M+Px@ezqa}*x9``likEFOektp@s~jpv%8hcX+$kO9L2*5MO48&0b5Kw6{aW7# z?NiixNm)`NWkIPZi^`Sqs89$XfB*srAbz?4pJ+z4I)00a;~ z009IL=ocv1v@Q29vw{EuBPmeUDelP`@*_C_KaBta2q2JKz}+bla=ANOs89vwLmfPx zdnWMt86N=z(gk+XTj)T*Q-Ox3b1@bI=>qOXNy;yQahtWOUn31^n(ZO&%eDw0fWUti gn9!}x?rvua0bc})HfJlDhMClT=Ifa{X$o858;!s?h5!Hn delta 93 zcmZo@U}|V!;+1%$%K!t6lNA}IMJ?DR*i0wi=ERUufXTqr|3?Fx6&de3Y&KwAZ3h6? C$Q}3q diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..73b2e5733595dbf04e90361187b1e1fa3225678f 100644 GIT binary patch literal 111272 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%0D4G`pW~7Ii*OpxRFDyCcrv|g+!-Iog9vu84 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 F{{r+U=~e&$ literal 0 HcmV?d00001 diff --git a/ka-note/server/src/lib/graph-service.ts b/ka-note/server/src/lib/graph-service.ts index d8920b6..95c0def 100644 --- a/ka-note/server/src/lib/graph-service.ts +++ b/ka-note/server/src/lib/graph-service.ts @@ -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 { + 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 { 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 { 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 { + 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; diff --git a/ka-note/server/src/routes/calendar.ts b/ka-note/server/src/routes/calendar.ts index 5574145..036eb3c 100644 --- a/ka-note/server/src/routes/calendar.ts +++ b/ka-note/server/src/routes/calendar.ts @@ -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);