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')); } }