azure login und cal sync

This commit is contained in:
beo3000 2026-02-27 16:30:32 +01:00
parent 2f9218ce0e
commit b6f35431df
5 changed files with 36 additions and 18 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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);