bearerToken(); if (blank($token)) { return null; } // Ours, not the identity server's: checked here and never sent there. if (str_starts_with($token, ApiToken::PREFIX)) { return $this->machine($token); } if (! $this->client->isConfigured()) { return null; } $identity = $this->resolveIdentity($token); if ($identity === null || ! $identity->isUsable()) { return null; } $user = $this->localUser($identity); // Locally deactivating an account revokes API access immediately, // without needing anything to change upstream. return $user->is_active ? $user : null; } protected function machine(string $plain): ?User { $token = ApiToken::findUsable($plain); $user = $token?->user; // A person switched off here takes their tokens with them. if ($user === null || ! $user->is_active) { return null; } $token->touchLastUsed(); return $user->withAccessToken($token); } protected function resolveIdentity(string $token): ?PireonIdentity { $key = 'pireon:introspect:'.hash('sha256', $token); $ttl = (int) config('pireon.introspection_ttl', 60); $cached = Cache::get($key); if (is_array($cached)) { return $cached === [] ? null : PireonIdentity::fromResponse($cached); } try { $identity = $this->client->identity($token); } catch (PireonException $e) { // Cache the rejection too, so a client hammering us with a dead // token cannot turn into a flood of upstream requests. Never cache // a transport failure — that would lock out valid tokens whenever // the identity server has a blip. if ($e->status === 401) { Cache::put($key, [], min($ttl, 60)); } return null; } Cache::put($key, $identity->toArray(), $ttl); return $identity; } /** * Mirror the identity locally so policies, ownership checks and foreign * keys have a real row to work with. Local roles are left untouched. */ protected function localUser(PireonIdentity $identity): User { $user = User::query()->where('oauth_id', $identity->id)->first(); return $user ?? $this->sync->handle($identity); } }