tenantId = (string) config('services.microsoft.tenant_id'); $this->clientId = (string) config('services.microsoft.client_id'); $this->clientSecret = (string) config('services.microsoft.client_secret'); $this->redirectUri = route('entra.callback'); } // ── Step 1: Redirect user to Microsoft login ────────────────────────────── public function connect(#[CurrentUser] User $user): RedirectResponse { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $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' => '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 { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug); if (! $hasAzureFdAccess) { abort(403, 'You do not have permission to access the required application.'); } // CSRF state check if (! hash_equals((string) session('oauth_state'), (string) $request->state)) { session()->forget('oauth_state'); return redirect()->route('dashboard.user') ->with('entra_error', 'Invalid OAuth state. Please try again.'); } session()->forget('oauth_state'); if ($request->has('error')) { return redirect()->route('dashboard.user') ->with('entra_error', $request->error_description ?? 'Microsoft login failed.'); } if (! $request->filled('code')) { return redirect()->route('dashboard.user') ->with('entra_error', 'No authorization code received from Microsoft.'); } // 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', 'scope' => self::SCOPES, ] ); if ($response->failed()) { return redirect()->route('dashboard.user') ->with('entra_error', $response->json('error_description', 'Failed to retrieve tokens from Microsoft.')); } $data = $response->json(); OauthToken::updateOrCreate( [ 'user_id' => $user->id, 'provider' => self::PROVIDER, ], [ 'access_token' => $data['access_token'], 'refresh_token' => $data['refresh_token'] ?? null, 'token_type' => $data['token_type'] ?? 'Bearer', 'scopes' => self::SCOPES, 'expires_at' => now()->addSeconds((int) ($data['expires_in'] ?? 3600)), ] ); return redirect()->route('dashboard.user') ->with('entra_success', 'Microsoft connected successfully!'); } // ── Open Outlook (silent refresh if expired) ────────────────────────────── public function openEntra(): RedirectResponse { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug); if (! $hasAzureFdAccess) { abort(403, 'You do not have permission to access the required application.'); } $token = $user->oauthToken(self::PROVIDER); if (! $token) { return redirect()->route('entra.connect'); } // Token still valid (5-min buffer) if ($token->expires_at->gt(now()->addMinutes(5))) { return redirect('https://outlook.office.com/mail/'); } // Token expired — try silent refresh if ($this->refreshToken($token)) { return redirect('https://outlook.office.com/mail/'); } // Refresh failed — force full re-login $token->delete(); return redirect()->route('entra.connect'); } // ── Disconnect ──────────────────────────────────────────────────────────── private function refreshToken(OauthToken $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((int) ($data['expires_in'] ?? 3600)), ]); return true; } // ── Internal: Refresh access token using refresh_token ─────────────────── public function disconnect(): RedirectResponse { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug); if (! $hasAzureFdAccess) { abort(403, 'You do not have permission to access the required application.'); } $user->oauthToken(self::PROVIDER)?->delete(); return redirect()->route('dashboard.user')->with('entra_success', 'Microsoft disconnected.'); } }