150 lines
5.0 KiB
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);
|
|
}
|
|
}
|