validate($bearerToken); if ($result === null) { return false; } $user = $result['user']; $userId = (int) ($user['id'] ?? 0); $userRoleRepository = self::userRoleRepository(); $rolePermissionRepository = self::rolePermissionRepository(); $userTenantContextService = self::userTenantContextService(); // Load permissions directly (bypass session cache) $roleIds = $userRoleRepository->listRoleIdsByUserId($userId); $permissions = $rolePermissionRepository->listPermissionKeysByRoleIds($roleIds); // Resolve tenant context: a token may be scoped to a specific tenant, // otherwise fall back to the user's active tenant context. $tokenTenantId = $result['tenant_id']; if ($tokenTenantId !== null) { // Verify the token's tenant is still assigned to this user $userTenantIds = $scopeGateway->getUserTenantIds($userId); if (!in_array($tokenTenantId, $userTenantIds, true)) { return false; } $currentTenantId = $tokenTenantId; self::$tokenTenantId = $tokenTenantId; } else { $currentTenantId = $userTenantContextService->getCurrentTenantId($userId); self::$tokenTenantId = null; } self::$currentUser = $user; self::$currentPermissions = $permissions; self::$currentTenantId = $currentTenantId; self::$currentTokenRecord = $result['token_record']; self::$authenticated = true; return true; } public static function isAuthenticated(): bool { return self::$authenticated; } public static function user(): ?array { return self::$currentUser; } public static function userId(): int { return (int) (self::$currentUser['id'] ?? 0); } public static function permissions(): array { return self::$currentPermissions ?? []; } public static function can(string $ability, array $context = []): bool { $decision = self::authorizationService()->authorize($ability, [ 'actor_user_id' => self::userId(), 'scoped_tenant_id' => self::scopedTenantId(), ...$context, ]); return $decision->isAllowed(); } // Effective tenant for data filtering — may come from token scope or user session. public static function tenantId(): ?int { return self::$currentTenantId; } // Non-null only when the token itself was issued for a specific tenant. // Used to restrict access in token-scoped requests (e.g. tenant integrations). public static function scopedTenantId(): ?int { return self::$tokenTenantId; } public static function tokenRecord(): ?array { return self::$currentTokenRecord; } public static function tokenId(): ?int { $id = (int) (self::$currentTokenRecord['id'] ?? 0); return $id > 0 ? $id : null; } public static function isTenantScopedToken(): bool { return (self::$tokenTenantId ?? 0) > 0; } /** * Require both tenant-scope and token-tenant access for a resource. */ public static function requireResourceAccess(string $resource, int $resourceId): void { $scopeGateway = self::scopeGateway(); if (!$scopeGateway->canAccess($resource, $resourceId, self::userId())) { ApiResponse::notFound(); } self::requireTokenTenantAccess($resource, $resourceId); } public static function requireTokenTenantAccess(string $resource, int $resourceId): void { if (!self::isTenantScopedToken()) { return; } $tenantId = (int) (self::$tokenTenantId ?? 0); if ($tenantId <= 0) { ApiResponse::forbidden(); } $scopeGateway = self::scopeGateway(); if (!$scopeGateway->resourceBelongsToTenant($resource, $resourceId, $tenantId)) { // 404 instead of 403 — don't reveal that the resource exists in another tenant ApiResponse::notFound(); } } /** * Check if the current user can self-manage API tokens. */ public static function canSelfManageTokens(): bool { $decision = self::authorizationService()->authorize( \MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_API_TOKENS_SELF_MANAGE, [ 'actor_user_id' => self::userId(), 'scoped_tenant_id' => self::scopedTenantId(), ] ); return $decision->isAllowed(); } public static function requireSelfManageTokens(): void { if (!self::canSelfManageTokens()) { ApiResponse::forbidden('api_tokens_self_manage_forbidden'); } } private static function scopeGateway(): TenantScopeService { return self::resolveDependency(self::$authScopeGatewayResolver, TenantScopeService::class, 'ApiAuth'); } private static function apiTokenService(): ApiTokenService { return self::resolveDependency(self::$apiTokenServiceResolver, ApiTokenService::class, 'ApiAuth'); } private static function userRoleRepository(): UserRoleRepository { return self::resolveDependency(self::$userRoleRepositoryResolver, UserRoleRepository::class, 'ApiAuth'); } private static function rolePermissionRepository(): RolePermissionRepository { return self::resolveDependency(self::$rolePermissionRepositoryResolver, RolePermissionRepository::class, 'ApiAuth'); } private static function userTenantContextService(): UserTenantContextService { return self::resolveDependency(self::$userTenantContextServiceResolver, UserTenantContextService::class, 'ApiAuth'); } private static function authorizationService(): AuthorizationService { return self::resolveDependency(self::$authorizationServiceResolver, AuthorizationService::class, 'ApiAuth'); } private static function resolveDependency(mixed $resolver, string $expectedClass, string $consumer): mixed { if (!is_callable($resolver)) { throw new \RuntimeException($consumer . ' is not configured for dependency: ' . $expectedClass); } $service = $resolver(); if (!$service instanceof $expectedClass) { throw new \RuntimeException($consumer . ' resolver returned invalid dependency: ' . $expectedClass); } return $service; } }