Referência v3
Voltar à plataforma

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.

Arquitetura Hexagonal

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:

Request ↓ TokenResolverInterface → De onde vem o token? ↓ TokenValidatorInterface → O token é válido? ↓ UserResolverInterface → Quem é o usuário? ↓ IdentityFactoryInterface → Como montar a identidade? ↓ AuthContextInterface → Orquestrador do pipeline ↓ AuthorizationInterface → O usuário pode fazer isso? ↓ AuthIdentityInterface → Objeto de identidade tipado

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.

php
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.

php
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

TipoDescriçãoStatus HTTP
userUsuário humano autenticado e ativo200
api_tokenToken machine-to-machine200
guestSem credencial válida401
inactiveCredencial válida, conta inativa403
not_foundToken válido, usuário não existe401

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.

php
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.

php
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.

php
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.

php
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.

php
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.

php
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:

1

Criar o módulo OAuth2Auth

src/Modules/OAuth2Auth/ ├── OAuth2TokenValidator.php ├── OAuth2Payload.php ├── OAuth2AuthContext.php └── OAuth2AuthProvider.php
2

Implementar TokenValidatorInterface

php
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
        // ...
    }
}
3

Registrar no provider

php
// 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
    );
}
Resultado

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.

1

Criar o módulo LdapAuth

src/Modules/LdapAuth/ ├── LdapUserResolver.php ├── LdapUser.php └── LdapAuthProvider.php
2

Implementar UserResolverInterface

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

Registrar no provider

php
// LdapAuth/LdapAuthProvider.php
public function boot(ContainerInterface $container): void
{
    $container->bind(
        UserResolverInterface::class,
        LdapUserResolver::class,
        true
    );
}
Resultado

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
php
// 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);
}
Desacoplamento total

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

php
use Src\Kernel\Auth\JwtAuthContext;

$auth = new JwtAuthContext(...);

Correto

php
use Src\Kernel\Contracts\AuthContextInterface;

public function __construct(
    private AuthContextInterface $auth
) {}

2. Use a fachada Auth nos controllers

Errado

php
$identity = $request->attribute('auth_identity');
$user = $request->attribute('auth_user');

Correto

php
$identity = Auth::identity($request);
$user = Auth::current($request);

3. Use type() em vez de instanceof

Errado

php
if ($identity instanceof InactiveAuthIdentity) {
    return Response::json(['error' => 'Conta inativa.'], 403);
}

Correto

php
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
Liberdade total

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.