185 lines
6.7 KiB
PHP
185 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\OutlookToken;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
class OutlookController extends Controller
|
|
{
|
|
// ── OAuth config from .env ────────────────────────────────────────────────
|
|
|
|
private string $tenantId;
|
|
private string $clientId;
|
|
private string $clientSecret;
|
|
private string $redirectUri;
|
|
|
|
private const SCOPES = 'openid profile email offline_access User.Read Mail.Read Mail.ReadWrite Mail.Send';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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;
|
|
}
|
|
} |