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

131 lines
4.4 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\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()->route('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();
}
try {
$token = $client->exchangeAuthorizationCode($authorizationCode);
$profile = $client->currentUser($token['access_token']);
} catch (Throwable $exception) {
Log::warning('DingTalk login failed while resolving the authorized user.', [
'exception' => $exception::class,
]);
return $this->failed();
}
$identity = DingTalkIdentity::query()
->with('user')
->where('union_id', $profile['union_id'])
->first();
if ($identity) {
return $this->loginBoundUser($request, $identity, $profile);
}
$bindingToken = Str::random(64);
$state->forceFill([
'binding_token_hash' => hash('sha256', $bindingToken),
'union_id' => $profile['union_id'],
'open_id' => $profile['open_id'],
'nickname' => $profile['nickname'],
'expires_at' => now()->addMinutes(max(1, (int) config('dingtalk.binding_ttl_minutes', 10))),
])->save();
return redirect()->route('login', ['dingtalk_binding' => $bindingToken])
->with('dingtalk_binding', trans('auth.dingtalk.confirm_binding'));
}
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,
'nickname' => $profile['nickname'] ?: $identity->nickname,
'last_logged_in_at' => now(),
])->save();
$url = route($user->landing_page, ['company_id' => $company->id]);
return redirect()->intended($url);
}
protected function failed(): RedirectResponse
{
return redirect()->route('login')->with('dingtalk_error', trans('auth.dingtalk.failed'));
}
}