CaiWuXiTong/app/Http/Controllers/Auth/DingTalkLogin.php

150 lines
5.0 KiB
PHP

<?php
namespace App\Http\Controllers\Auth;
use App\Abstracts\Http\Controller;
use App\Models\Auth\DingTalkIdentity;
use App\Models\Auth\DingTalkOAuthState;
use App\Services\Auth\DingTalkClosedAccess;
use App\Services\Auth\DingTalkOAuthClient;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
class DingTalkLogin extends Controller
{
public function redirect(DingTalkOAuthClient $client): RedirectResponse
{
if (! $client->isConfigured()) {
return redirect()->to($this->applicationRoute('login'))
->with('dingtalk_error', trans('auth.dingtalk.unavailable'));
}
DingTalkOAuthState::query()
->where('expires_at', '<', now()->subDay())
->delete();
$state = Str::random(64);
DingTalkOAuthState::query()->create([
'state_hash' => hash('sha256', $state),
'expires_at' => now()->addMinutes(max(1, (int) config('dingtalk.state_ttl_minutes', 5))),
]);
return redirect()->away($client->authorizationUrl($state));
}
public function callback(Request $request, DingTalkOAuthClient $client): RedirectResponse
{
$stateValue = trim((string) $request->query('state', ''));
$authorizationCode = trim((string) ($request->query('authCode') ?: $request->query('code', '')));
if ($stateValue === '' || $authorizationCode === '') {
return $this->failed();
}
$state = DB::transaction(function () use ($stateValue): ?DingTalkOAuthState {
$candidate = DingTalkOAuthState::query()
->where('state_hash', hash('sha256', $stateValue))
->lockForUpdate()
->first();
if (! $candidate || $candidate->consumed_at || $candidate->expires_at->isPast()) {
return null;
}
$candidate->forceFill(['consumed_at' => now()])->save();
return $candidate;
});
if (! $state) {
return $this->failed();
}
$oauthProfile = null;
try {
$token = $client->exchangeAuthorizationCode($authorizationCode);
$oauthProfile = $client->currentUser($token['access_token']);
$profile = $client->enterpriseUser($oauthProfile);
} catch (Throwable $exception) {
Log::warning('DingTalk login failed while resolving the enterprise user.', [
'exception' => $exception::class,
]);
app(DingTalkClosedAccess::class)->audit(
'login_denied',
'denied',
null,
null,
$oauthProfile['union_id'] ?? null,
$request,
['reason' => 'enterprise_identity_unavailable']
);
return $this->denied();
}
$identity = app(DingTalkClosedAccess::class)->resolve($profile, $request);
if ($identity) {
return $this->loginBoundUser($request, $identity, $profile);
}
return $this->denied();
}
protected function loginBoundUser(Request $request, DingTalkIdentity $identity, array $profile): RedirectResponse
{
$user = $identity->user;
$company = $user?->withoutEvents(fn () => $user->companies()->enabled()->first());
if (! $user || ! $user->enabled || ! $company) {
Log::info('DingTalk login denied for a disabled or unassigned account.', [
'identity_id' => $identity->id,
]);
return $this->failed();
}
Auth::login($user, true);
$request->session()->regenerate();
$identity->forceFill([
'open_id' => $profile['open_id'] ?: $identity->open_id,
'dingtalk_user_id' => $profile['dingtalk_user_id'] ?: $identity->dingtalk_user_id,
'nickname' => $profile['nickname'] ?: $identity->nickname,
'last_logged_in_at' => now(),
])->save();
app(DingTalkClosedAccess::class)->recordLogin($identity, $request);
$url = $this->applicationRoute($user->landing_page, ['company_id' => $company->id]);
return redirect()->intended($url);
}
protected function failed(): RedirectResponse
{
return redirect()->to($this->applicationRoute('login'))
->with('dingtalk_error', trans('auth.dingtalk.failed'));
}
protected function denied(): RedirectResponse
{
return redirect()->to($this->applicationRoute('login'))
->with('dingtalk_error', trans('auth.dingtalk.denied'));
}
protected function applicationRoute(string $name, array $parameters = []): string
{
$path = route($name, $parameters, false);
$applicationUrl = rtrim((string) config('app.url'), '/');
return $applicationUrl !== ''
? $applicationUrl.'/'.ltrim($path, '/')
: route($name, $parameters);
}
}