tenantId = config('services.microsoft.tenant_id'); $this->clientId = config('services.microsoft.client_id'); $this->clientSecret = config('services.microsoft.client_secret'); $this->redirectUri = route('outlook.callback'); } // ── Test page ───────────────────────────────────────────────────────────── public function index(): View { $token = auth()->user()?->outlookToken; $connected = $token && ! $token->isExpired(); $outlookUrl = null; if ($connected) { // Silent redirect URL — opens Outlook web with the user already signed in // via SSO cookie / session; no token is passed in the URL (not safe). // Instead we redirect to Outlook and the browser SSO session handles it. $outlookUrl = 'https://outlook.office.com/mail/'; } return view('test.outlook', compact('connected', 'outlookUrl', 'token')); } // ── Step 1: Redirect user to Microsoft login ────────────────────────────── public function connect(): RedirectResponse { $state = Str::random(40); session(['oauth_state' => $state]); $query = http_build_query([ 'client_id' => $this->clientId, 'response_type' => 'code', 'redirect_uri' => $this->redirectUri, 'response_mode' => 'query', 'scope' => self::SCOPES, 'state' => $state, // prompt=none tries silent login first; falls back to login page if needed 'prompt' => 'select_account', ]); return redirect("https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/authorize?{$query}"); } // ── Step 2: Handle callback, exchange code for tokens ──────────────────── public function callback(Request $request): RedirectResponse { // CSRF state check if ($request->state !== session('oauth_state')) { return redirect()->route('outlook.test')->with('error', 'Invalid OAuth state. Please try again.'); } if ($request->has('error')) { return redirect()->route('outlook.test')->with('error', $request->error_description ?? 'Microsoft login failed.'); } // Exchange authorization code for tokens $response = Http::asForm()->post( "https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token", [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->code, 'redirect_uri' => $this->redirectUri, 'grant_type' => 'authorization_code', ] ); if ($response->failed()) { return redirect()->route('outlook.test')->with('error', 'Failed to retrieve tokens from Microsoft.'); } $data = $response->json(); // Persist token for this user (upsert so re-connecting just updates) OutlookToken::updateOrCreate( ['user_id' => auth()->id()], [ 'access_token' => $data['access_token'], 'refresh_token' => $data['refresh_token'] ?? null, 'token_type' => $data['token_type'] ?? 'Bearer', 'expires_at' => now()->addSeconds($data['expires_in'] ?? 3600), ] ); return redirect()->route('outlook.test')->with('success', 'Outlook connected successfully!'); } // ── Open Outlook (refresh token silently if expired) ───────────────────── public function openOutlook(): RedirectResponse { $token = auth()->user()?->outlookToken; if (! $token) { return redirect()->route('outlook.connect'); } // If token is expired, try to refresh silently if ($token->isExpired()) { $refreshed = $this->refreshToken($token); if (! $refreshed) { // Refresh failed — force re-login $token->delete(); return redirect()->route('outlook.connect'); } } // Redirect to Outlook Web — SSO session handles silent sign-in return redirect('https://outlook.office.com/mail/'); } // ── Disconnect ──────────────────────────────────────────────────────────── public function disconnect(): RedirectResponse { auth()->user()?->outlookToken?->delete(); return redirect()->route('outlook.test')->with('success', 'Outlook disconnected.'); } // ── Internal: Refresh access token using refresh_token ─────────────────── private function refreshToken(OutlookToken $token): bool { if (! $token->refresh_token) { return false; } $response = Http::asForm()->post( "https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token", [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'refresh_token' => $token->refresh_token, 'grant_type' => 'refresh_token', 'scope' => self::SCOPES, ] ); if ($response->failed()) { return false; } $data = $response->json(); $token->update([ 'access_token' => $data['access_token'], 'refresh_token' => $data['refresh_token'] ?? $token->refresh_token, 'expires_at' => now()->addSeconds($data['expires_in'] ?? 3600), ]); return true; } }