wappcode / gql-pdss-auth
Requires
- php: >=8.2
- firebase/php-jwt: ^7.0
- guzzlehttp/guzzle: ^7.8
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.6
- wappcode/gqlpdss: dev-master
- wappcode/graphql-basic-client: ^1.0
- wappcode/pdss-utilities: dev-master
README
Librería completa de autenticación y autorización para aplicaciones con WAppCore (GQLPDSS). Provee servicios de autenticación mediante sesión PHP y JWT con un sistema robusto de roles y permisos.
📋 Tabla de Contenidos
- Instalación
- Configuración Básica
- Uso Rápido
- AuthService
- Sistema de Roles y Permisos
- Protección de Resolvers GraphQL
- Middleware de Autenticación
- JWT Authentication
- API Reference
🚀 Instalación
1. Instalación via Composer
composer require wappcode/gql-pdss-auth
2. Configuración de Entidades Doctrine
Agregue las entidades del paquete a la configuración de Doctrine en config/local.config.php:
<?php return [ 'database' => [ 'connection' => [ 'driver' => 'pdo_mysql', 'user' => 'usuario', 'password' => 'password', 'host' => 'localhost', 'port' => 3306, 'dbname' => 'app', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci' ], "entity_paths" => [ realpath(__DIR__ . "/../../dev/modules/AppModule/Entities"), realpath(__DIR__ . "/../../vendor/wappcode/gql-pdss-auth/GPDAuth/src/Entities"), ] ] ];
3. Crear Esquema de Base de Datos
Opción A: CLI de Doctrine
vendor/bin/doctrine orm:schema-tool:create
Opción B: Script SQL
Ejecute el archivo SQL incluido: sql/gpd_auth.sql
⚙️ Configuración Básica
Integración con GQLPDSS
Configure el módulo en su aplicación (normalmente en dev/public/index.php):
<?php use GPDAuth\GPDAuthModule; // Configuración básica (recomendada para GraphQL) $app->addModules([ new GPDAuthModule( exitUnauthenticated: false, // Para GraphQL, permite validación granular por resolver publicRoutes: ['/login', '/register'] // Rutas que no requieren autenticación ), // Otros módulos... AppModule::class, ]);
Configuración de Módulos
<?php // Para aplicaciones REST API tradicionales new GPDAuthModule( exitUnauthenticated: true, // Responde 401 si no está autenticado publicRoutes: ['/login', '/register', '/forgot-password'] ); // Para aplicaciones GraphQL (recomendado) new GPDAuthModule( exitUnauthenticated: false, // Permite validación a nivel de resolver publicRoutes: ['/login'] );
🏃♂️ Uso Rápido
Autenticación Básica
<?php use GPDAuth\Contracts\AuthServiceInterface; // En un resolver o controlador $authService = $context->getServiceManager()->get(AuthServiceInterface::class); // Login try { $authService->login('username', 'password', 'local_user'); echo "Login exitoso"; } catch (Exception $e) { echo "Error: " . $e->getMessage(); } // Verificar si está autenticado $user = $authService->getAuthenticatedUser(); if ($user) { echo "Usuario: " . $user->getUsername(); } // Logout $authService->logout();
GraphQL Integration
<?php // En un módulo GraphQL use GPDAuth\Graphql\AuthResolverGuardFactory; use GPDCore\Graphql\ResolverPipelineFactory; class AppModule extends AbstractModule { function getResolvers(): array { $echoResolve = fn($root, $args) => $args["message"]; return [ // Resolver público "Query::echo" => $echoResolve, // Resolver protegido que requiere autenticación 'Query::echoProtected' => ResolverPipelineFactory::createPipeline($echoResolve, [ AuthResolverGuardFactory::requireAuthenticated(), ]), // Resolver que requiere rol específico 'Query::adminOnly' => ResolverPipelineFactory::createPipeline($someResolver, [ AuthResolverGuardFactory::requireRole('admin'), ]), ]; } }
🔐 AuthService
El AuthServiceInterface es el corazón del sistema de autenticación:
Métodos Principales
<?php use GPDAuth\Contracts\AuthServiceInterface; $auth = $context->getServiceManager()->get(AuthServiceInterface::class); // Autenticación $auth->login(string $username, string $password, string $grantType): void; $auth->logout(): void; $user = $auth->getAuthenticatedUser(): ?AuthenticatedUserInterface;
Ejemplo Práctico en GraphQL
<?php // Resolver de login public static function createLoginResolve(): callable { return function ($root, array $args, AppContextInterface $context, $info) { $username = $args["username"] ?? ''; $password = $args["password"] ?? ''; /** @var AuthServiceInterface */ $auth = $context->getServiceManager()->get(AuthServiceInterface::class); try { $auth->login($username, $password, AuthenticatedUserType::LOCAL_USER->value); $user = $auth->getAuthenticatedUser(); return [ 'success' => true, 'user' => $user->toArray(), 'message' => 'Login exitoso' ]; } catch (Throwable $e) { throw new GQLException('Credenciales inválidas', 'INVALID_CREDENTIALS'); } }; }
👥 Sistema de Roles y Permisos
Interfaz AuthenticatedUserInterface
<?php // Verificación de roles $user->hasRole(string $role): bool; $user->hasAnyRole(array $roles): bool; $user->hasAllRoles(array $roles): bool; // Verificación de permisos $user->hasPermission(string $resource, string $permission, ?string $scope = null): bool; $user->hasAnyPermission(array $resources, array $permissions, ?array $scopes = null): bool; $user->hasAllPermissions(array $resources, array $permissions, ?array $scopes = null): bool;
Ejemplos de Uso
<?php $user = $auth->getAuthenticatedUser(); // Verificar roles if ($user->hasRole('admin')) { echo "Usuario es administrador"; } if ($user->hasAnyRole(['editor', 'publisher'])) { echo "Usuario puede editar contenido"; } // Verificar permisos específicos if ($user->hasPermission('posts', 'CREATE')) { echo "Usuario puede crear posts"; } // Permisos con scope if ($user->hasPermission('posts', 'EDIT', 'OWNER')) { echo "Usuario puede editar sus propios posts"; } if ($user->hasPermission('posts', 'EDIT', 'ALL')) { echo "Usuario puede editar cualquier post"; } // Permisos múltiples if ($user->hasAllPermissions(['posts', 'comments'], ['CREATE', 'EDIT'], ['ALL'])) { echo "Usuario tiene control completo sobre posts y comentarios"; }
🛡️ Protección de Resolvers GraphQL
AuthResolverGuardFactory
Protege resolvers GraphQL con diferentes niveles de autorización:
<?php use GPDAuth\Graphql\AuthResolverGuardFactory; // Requiere autenticación (cualquier usuario logueado) AuthResolverGuardFactory::requireAuthenticated(); // Requiere rol específico AuthResolverGuardFactory::requireRole('admin'); // Requiere cualquiera de los roles AuthResolverGuardFactory::requireAnyRole(['editor', 'publisher']); // Requiere todos los roles AuthResolverGuardFactory::requireAllRoles(['staff', 'verified']); // Requiere permiso específico AuthResolverGuardFactory::requirePermission('posts', 'CREATE'); // Requiere permiso con scope AuthResolverGuardFactory::requirePermission('posts', 'EDIT', 'OWNER'); // Requiere cualquier permiso de la lista AuthResolverGuardFactory::requireAnyPermission( ['posts', 'pages'], ['CREATE', 'EDIT'], ['ALL', 'OWNER'] ); // Requiere todos los permisos AuthResolverGuardFactory::requireAllPermissions( ['posts'], ['CREATE', 'EDIT', 'DELETE'], ['ALL'] );
Ejemplo Completo de Protección
<?php class AppModule extends AbstractModule { function getResolvers(): array { return [ // Público 'Query::login' => FieldLogin::createResolve(), // Solo usuarios autenticados 'Query::profile' => ResolverPipelineFactory::createPipeline( $profileResolver, [AuthResolverGuardFactory::requireAuthenticated()] ), // Solo administradores 'Mutation::deleteUser' => ResolverPipelineFactory::createPipeline( $deleteUserResolver, [AuthResolverGuardFactory::requireRole('admin')] ), // Editores o publicadores 'Mutation::publishPost' => ResolverPipelineFactory::createPipeline( $publishResolver, [AuthResolverGuardFactory::requireAnyRole(['editor', 'publisher'])] ), // Permiso específico con scope 'Mutation::editPost' => ResolverPipelineFactory::createPipeline( $editPostResolver, [AuthResolverGuardFactory::requirePermission('posts', 'EDIT', 'OWNER')] ), ]; } }
🔒 Middleware de Autenticación
AuthMiddleware
El middleware valida automáticamente las solicitudes:
<?php // Configuración en GPDAuthModule new GPDAuthModule( exitUnauthenticated: true, // true: responde 401 si no autenticado // false: continúa y permite validación granular publicRoutes: ['/login', '/register', '/forgot-password'] );
Comportamiento del Middleware
-
exitUnauthenticated: true - Ideal para APIs REST
- Responde inmediatamente con 401 si no está autenticado
- Protege todas las rutas excepto las públicas
-
exitUnauthenticated: false - Ideal para GraphQL
- Permite que las solicitudes continúen
- AuthResolverGuardFactory maneja la validación por resolver
Acceso al Usuario en Request
<?php // El middleware inyecta el usuario autenticado en el request $request = $context->getContextAttribute(ServerRequestInterface::class); $user = $request->getAttribute(AuthenticatedUserInterface::class); if ($user instanceof AuthenticatedUserInterface) { echo "Usuario autenticado: " . $user->getUsername(); }
🎫 JWT Authentication
Módulo GPDAuthJWT
Para autenticación JWT, use el módulo adicional:
<?php use GPDAuthJWT\GPDAuthJWTModule; // Configurar junto con el módulo base $app->addModules([ new GPDAuthModule(exitUnauthenticated: false), new GPDAuthJWTModule(), AppModule::class, ]);
JWT vs Session
- Session Auth: Usa sesiones PHP, ideal para aplicaciones web tradicionales
- JWT Auth: Tokens sin estado, ideal para APIs y aplicaciones SPA/móviles
OAuth 2.0 Client Credentials
<?php // Endpoint para obtener tokens JWT // POST /oauth/token $response = [ 'grant_type' => 'client_credentials', 'client_id' => 'your_client_id', 'client_secret' => 'your_client_secret', 'scope' => 'read write' ];
Configuración Apache para JWT
Para que la autenticación JWT funcione correctamente con Apache, es necesario configurar el servidor para que pase el header Authorization:
# En VirtualHost o .htaccess SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 # O usando RewriteRule RewriteEngine On RewriteCond %{HTTP:Authorization} ^(.*) RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
📚 API Reference
Interfaces Principales
AuthServiceInterface
interface AuthServiceInterface { public function login(string $username, string $password, string $grantType); public function logout(): void; public function getAuthenticatedUser(): ?AuthenticatedUserInterface; }
AuthenticatedUserInterface
interface AuthenticatedUserInterface { // Información del usuario public function getId(): string; public function getUsername(): string; public function getFullName(): string; public function getEmail(): ?string; public function toArray(): array; // Roles public function hasRole(string $role): bool; public function hasAnyRole(array $roles): bool; public function hasAllRoles(array $roles): bool; // Permisos public function hasPermission(string $resource, string $permission, ?string $scope = null): bool; public function hasAnyPermission(array $resources, array $permission, ?array $scopes = null): bool; public function hasAllPermissions(array $resources, array $permission, ?array $scopes = null): bool; }
GraphQL Schema
El módulo proporciona los siguientes tipos y queries GraphQL:
type Query { login(username: String!, password: String!): AuthenticatedUser getSessionData: AuthenticatedUser } type AuthenticatedUser { fullName: String! firstName: String! lastName: String username: String! email: String picture: String roles: [String!]! permissions: [String!]! }
Enums y Tipos
<?php // Tipos de usuario enum AuthenticatedUserType: string { case API_CLIENT = 'api_client'; case LOCAL_USER = 'local_user'; case EXTERN_USER = 'extern_user'; } // Métodos de autenticación enum AuthMethod: string { case Session = 'SESSION'; case Jwt = 'JWT'; case JwtOrSession = 'JWT_OR_SESSION'; case SessionOrJwt = 'SESSION_OR_JWT'; }
Entidades de Base de Datos
La librería incluye las siguientes entidades Doctrine:
- User: usuarios del sistema
- Role: roles asignables a usuarios
- Resource: recursos protegidos
- Permission: permisos específicos sobre recursos
- ApiConsumer: clientes API para JWT
- TrustedIssuer: emisores JWT confiables
💡 Ejemplos Adicionales
Personalización de Resolvers con Usuario
<?php $proxyEcho = fn($resolver) => function ($root, $args, AppContextInterface $context, $info) use ($resolver) { /** @var AuthServiceInterface */ $authService = $context->getServiceManager()->get(AuthServiceInterface::class); $user = $authService->getAuthenticatedUser(); if (!$user) { return $resolver($root, $args, $context, $info); } $msg = $resolver($root, $args, $context, $info); return sprintf("%s -> Usuario: %s", $msg, $user->getUsername()); }; return [ 'Query::echoWithUser' => ResolverPipelineFactory::createPipeline($echoResolve, [ ResolverPipelineFactory::createWrapper($proxyEcho), AuthResolverGuardFactory::requireAuthenticated(), ]), ];
Manejo de Errores
<?php use GPDAuth\Library\NoSignedException; use GPDAuth\Library\NoAuthorizedException; try { $user = static::getAuthenticatedUser($context); if (!$user) { throw new NoSignedException(); } if (!$user->hasRole('admin')) { throw new NoAuthorizedException("Acceso denegado", "FORBIDDEN", 403); } // Lógica del resolver... } catch (NoSignedException $e) { throw new GQLException('Debe iniciar sesión', 'UNAUTHENTICATED', 401); } catch (NoAuthorizedException $e) { throw new GQLException('Permisos insuficientes', 'FORBIDDEN', 403); }
Uso sin GQLPDSS
Para usar la librería en proyectos sin GQLPDSS:
composer require wappcode/gql-pdss-auth
Agregue las entidades a la configuración de Doctrine:
$entityPaths = [ __DIR__ . "/../vendor/wappcode/gql-pdss-auth/GPDAuth/src/Entities" ];
Cree una instancia del servicio:
<?php use GPDAuth\Services\AuthSessionService; use GPDAuth\Services\UserRepository; $userRepository = new UserRepository($entityManager); $authService = new AuthSessionService($userRepository); // Uso del servicio $authService->login('username', 'password', 'local_user'); $user = $authService->getAuthenticatedUser();
Este sistema de autenticación proporciona una base sólida y flexible para manejar la seguridad en aplicaciones PHP modernas con GraphQL y REST APIs.
🤝 Contribución
Para contribuir al proyecto:
- Fork el repositorio
- Crea una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add some amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
📄 Licencia
Este proyecto está licenciado bajo la Licencia MIT - ver el archivo LICENSE para más detalles.