Autenticação Plugável
Sistema completamente plugável baseado em contratos. Substitua JWT por OAuth2, LDAP ou qualquer estratégia sem modificar o kernel.
Visão Geral
A Vupi.us API implementa um sistema de autenticação completamente plugável baseado em contratos (interfaces). Isso significa que desenvolvedores podem substituir qualquer parte do pipeline de autenticação — desde a extração do token até a resolução do usuário — sem modificar o kernel.
O sistema segue os princípios de Ports & Adapters e Dependency Inversion: o kernel define portas (contratos), os módulos fornecem adaptadores (implementações), e módulos de negócio consomem as portas sem saber qual adaptador está ativo.
Cenários possíveis
- Substituir JWT por OAuth2, SAML, magic link ou sessão PHP
- Usar LDAP, Active Directory ou API externa para resolver usuários
- Implementar autorização baseada em ACL, RBAC ou ABAC
- Integrar múltiplos módulos de autenticação diferentes no mesmo projeto
Pipeline de Autenticação
O pipeline é composto por 8 contratos independentes, cada um com uma responsabilidade única:
Cada contrato pode ser substituído independentemente por um módulo externo.
Os 8 Contratos
1. AuthContextInterface
Responsabilidade: Orquestrar o pipeline completo de autenticação.
interface AuthContextInterface
{
// Constantes para atributos do Request
public const IDENTITY_KEY = 'auth_identity';
public const LEGACY_USER_KEY = 'auth_user';
public const LEGACY_PAYLOAD_KEY = 'auth_payload';
/**
* Resolve a identidade a partir do Request.
* Retorna AuthIdentityInterface se autenticado, null se não há credencial válida.
*/
public function resolve(Request $request): ?AuthIdentityInterface;
/**
* Extrai a identidade já resolvida de um Request.
*/
public function identity(Request $request): ?AuthIdentityInterface;
}
Implementação nativa: JwtAuthContext
2. AuthIdentityInterface
Responsabilidade: Representar a identidade autenticada de forma tipada e imutável.
interface AuthIdentityInterface
{
// Identificação
public function id(): string|int|null;
public function role(): ?string;
public function type(): string; // 'user', 'api_token', 'guest', 'inactive', etc.
// Verificações de estado
public function isAuthenticated(): bool;
public function isApiToken(): bool;
public function isGuest(): bool;
public function hasRole(string ...$roles): bool;
// Escape hatches (low-level)
public function user(): mixed;
public function payload(): ?TokenPayloadInterface;
}
Implementações nativas: AuthIdentity, InactiveAuthIdentity, NotFoundAuthIdentity
Tipos de identidade
| Tipo | Descrição | Status HTTP |
|---|---|---|
user | Usuário humano autenticado e ativo | 200 |
api_token | Token machine-to-machine | 200 |
guest | Sem credencial válida | 401 |
inactive | Credencial válida, conta inativa | 403 |
not_found | Token válido, usuário não existe | 401 |
Módulos podem definir tipos adicionais: service, bot, impersonated, etc.
3. AuthorizationInterface
Responsabilidade: Decidir se uma identidade tem permissão para realizar uma ação.
interface AuthorizationInterface
{
/**
* Verifica se a identidade tem permissão de administrador do sistema.
*/
public function isAdmin(AuthIdentityInterface $identity, Request $request): bool;
/**
* Verifica se a identidade possui um dos papéis informados.
*/
public function hasRole(AuthIdentityInterface $identity, string ...$roles): bool;
}
4. TokenResolverInterface
Responsabilidade: Extrair o token bruto do Request.
interface TokenResolverInterface
{
/**
* Extrai o token bruto do Request.
* Retorna string vazia se não encontrar token.
*/
public function resolve(Request $request): string;
}
Implementações nativas: BearerTokenResolver, CookieTokenResolver, CompositeTokenResolver
5. TokenValidatorInterface
Responsabilidade: Validar o token e retornar o payload tipado.
interface TokenValidatorInterface
{
/**
* Valida o token e retorna o payload tipado, ou null se inválido.
*/
public function validate(string $token): ?TokenPayloadInterface;
/**
* Indica se o token é um token de API puro (machine-to-machine).
*/
public function isApiToken(string $token): bool;
}
Implementação nativa: JwtTokenValidator
6. TokenPayloadInterface
Responsabilidade: Encapsular o payload do token de forma tipada.
interface TokenPayloadInterface
{
public function getSubject(): ?string;
public function getRole(): ?string;
public function isSignedWithApiSecret(): bool;
public function get(string $key): mixed;
public function raw(): mixed;
}
7. UserResolverInterface
Responsabilidade: Buscar o usuário pelo identificador extraído do payload.
interface UserResolverInterface
{
/**
* Resolve o usuário pelo identificador do payload.
* Retorna null se não encontrado.
*/
public function resolve(string $identifier, TokenPayloadInterface $payload): mixed;
}
Implementação nativa: DatabaseUserResolver
8. IdentityFactoryInterface
Responsabilidade: Criar objetos AuthIdentityInterface a partir de usuário e payload.
interface IdentityFactoryInterface
{
/**
* Cria uma identidade para um usuário autenticado.
*/
public function forUser(mixed $user, TokenPayloadInterface $payload): AuthIdentityInterface;
/**
* Cria uma identidade para um token de API puro (machine-to-machine).
*/
public function forApiToken(): AuthIdentityInterface;
}
Implementação nativa: DefaultIdentityFactory
Exemplo: Substituir JWT por OAuth2
Um desenvolvedor quer usar OAuth2 em vez de JWT. Veja como fazer:
Criar o módulo OAuth2Auth
Implementar TokenValidatorInterface
final class OAuth2TokenValidator implements TokenValidatorInterface
{
public function __construct(
private string $introspectionEndpoint,
private string $clientId,
private string $clientSecret
) {}
public function validate(string $token): ?TokenPayloadInterface
{
// Chama o endpoint de introspecção do OAuth2
$response = $this->introspect($token);
if (!$response['active']) {
return null;
}
return new OAuth2Payload($response);
}
public function isApiToken(string $token): bool
{
return str_starts_with($token, 'client_');
}
private function introspect(string $token): array
{
// Implementação da chamada HTTP ao servidor OAuth2
// ...
}
}
Registrar no provider
// OAuth2Auth/OAuth2AuthProvider.php
public function boot(ContainerInterface $container): void
{
// Substitui o validator
$container->bind(
TokenValidatorInterface::class,
OAuth2TokenValidator::class,
true
);
// Substitui o context
$container->bind(
AuthContextInterface::class,
OAuth2AuthContext::class,
true
);
}
Todos os módulos que usam Auth::user(), Auth::admin(), Auth::identity() continuam funcionando — agora com OAuth2 em vez de JWT.
Exemplo: Usar LDAP para resolver usuários
Um desenvolvedor quer autenticar usuários via LDAP em vez do banco de dados.
Criar o módulo LdapAuth
Implementar UserResolverInterface
final class LdapUserResolver implements UserResolverInterface
{
public function __construct(private LdapConnection $ldap) {}
public function resolve(string $identifier, TokenPayloadInterface $payload): mixed
{
$dn = "uid={$identifier},ou=users,dc=example,dc=com";
$result = ldap_read($this->ldap->connection(), $dn, "(objectClass=*)");
if (!$result) {
return null;
}
$entries = ldap_get_entries($this->ldap->connection(), $result);
$entry = $entries[0] ?? [];
return new LdapUser(
uid: $entry['uid'][0] ?? '',
name: $entry['cn'][0] ?? '',
email: $entry['mail'][0] ?? '',
role: $entry['role'][0] ?? 'user'
);
}
}
Registrar no provider
// LdapAuth/LdapAuthProvider.php
public function boot(ContainerInterface $container): void
{
$container->bind(
UserResolverInterface::class,
LdapUserResolver::class,
true
);
}
O JWT continua sendo usado para validar tokens, mas os usuários são buscados no LDAP em vez do banco de dados.
Integração de Múltiplos Módulos
Um desenvolvedor cria três módulos independentes:
- MeuAuth — módulo de autenticação com OAuth2
- MeuUsers — módulo de usuários com tabela própria
- MeuEcommerce — módulo de e-commerce que precisa de autenticação
// MeuAuth/MeuAuthProvider.php
public function boot(ContainerInterface $container): void
{
$container->bind(AuthContextInterface::class, OAuth2AuthContext::class, true);
$container->bind(TokenValidatorInterface::class, OAuth2TokenValidator::class, true);
}
// MeuUsers/MeuUsersProvider.php
public function boot(ContainerInterface $container): void
{
$container->bind(UserRepositoryInterface::class, MeuUserRepository::class, true);
$container->bind(UserResolverInterface::class, MeuUserResolver::class, true);
}
// MeuEcommerce/Routes/web.php
use Src\Kernel\Auth;
$router->get('/api/pedidos', [PedidoController::class, 'listar'], Auth::user());
$router->post('/api/pedidos', [PedidoController::class, 'criar'], Auth::user());
// MeuEcommerce/Controllers/PedidoController.php
public function criar(Request $request): Response
{
$identity = Auth::identity($request);
$userId = Auth::id($request);
$role = Auth::role($request);
// Funciona independente de qual módulo de auth está ativo
$pedido = $this->service->criar($userId, $request->body());
return Response::json(['data' => $pedido], 201);
}
MeuEcommerce não sabe nada sobre MeuAuth ou MeuUsers. Ele só usa os contratos do kernel. Se amanhã o desenvolvedor trocar MeuAuth por outra solução, MeuEcommerce não muda uma linha.
Boas Práticas
1. Sempre use os contratos, nunca as implementações
Errado
use Src\Kernel\Auth\JwtAuthContext;
$auth = new JwtAuthContext(...);
Correto
use Src\Kernel\Contracts\AuthContextInterface;
public function __construct(
private AuthContextInterface $auth
) {}
2. Use a fachada Auth nos controllers
Errado
$identity = $request->attribute('auth_identity');
$user = $request->attribute('auth_user');
Correto
$identity = Auth::identity($request);
$user = Auth::current($request);
3. Use type() em vez de instanceof
Errado
if ($identity instanceof InactiveAuthIdentity) {
return Response::json(['error' => 'Conta inativa.'], 403);
}
Correto
if ($identity->type() === 'inactive') {
return Response::json(['error' => 'Conta inativa.'], 403);
}
Resumo
A arquitetura de autenticação plugável da Vupi.us API permite que desenvolvedores:
- Substituam qualquer parte do pipeline — token resolver, validator, user resolver, identity factory, authorization
- Integrem múltiplos módulos — um módulo de auth + um módulo de users + N módulos de negócio
- Usem qualquer estratégia de autenticação — JWT, OAuth2, SAML, LDAP, sessão, magic link, etc.
- Implementem autorização customizada — ACL, RBAC, ABAC, policies, etc.
- Mantenham compatibilidade — módulos de negócio usam contratos, não implementações
Tudo isso sem modificar uma linha do kernel. Desenvolvedores possuem total liberdade para criar módulos de autenticação próprios e integrá-los com outros módulos sem acoplamento.