YourBOX Authentication
Purpose
Section titled “Purpose”ADR-0004 sets the identity model: Authentik OIDC in production, Symfony form login in development and on the VPS smoke test, with a shared local user table that mirrors authenticated subjects. This page documents the firewall layout, the user lifecycle, the role model and the security defenses.
Firewall Layout
Section titled “Firewall Layout”Two Symfony Security firewalls live side by side. Only one is active per request, selected by the URL pattern.
security: password_hashers: App\Identity\Domain\User: 'auto' # Argon2id providers: users: entity: class: App\Identity\Domain\User property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false
oidc: pattern: ^/(oidc|app|admin) stateless: false oauth2_client: client: authentik check_path: /oidc/callback user_provider: service: App\Identity\Infrastructure\AuthentikUserProvider logout: path: /oidc/logout target: '/'
form: pattern: ^/(login|register|reset|profile) stateless: false form_login: login_path: login check_path: login_check enable_csrf: true logout: path: logout target: '/' remember_me: secret: '%env(APP_SECRET)%' lifetime: 604800 samesite: strict
access_control: - { path: ^/health, roles: PUBLIC_ACCESS } - { path: ^/metrics, roles: PUBLIC_ACCESS } - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/app, roles: ROLE_USER } - { path: ^/profile, roles: ROLE_USER }The form firewall is only loaded when APP_ENV equals dev or vps. The Symfony
configuration uses environment conditional loading:
when@dev: &dev_form security: firewalls: form: pattern: ^/(login|register|reset|profile) ...when@vps: *dev_formShared User Entity
Section titled “Shared User Entity”The App\Identity\Domain\User entity carries the columns needed by both firewalls and
by every bounded context that references a user. The OIDC path populates oidc_sub,
the form path populates password. Both paths populate username and email.
namespace App\Identity\Domain;
#[ORM\Entity, ORM\Table(name: 'users')]class User implements UserInterface, PasswordAuthenticatedUserInterface{ #[ORM\Id, ORM\Column(type: 'ulid')] private Ulid $id;
#[ORM\Column(length: 64, unique: true)] private string $username;
#[ORM\Column(length: 255, unique: true)] private string $email;
#[ORM\Column(length: 191, unique: true, nullable: true)] private ?string $oidcSub = null;
#[ORM\Column(length: 255, nullable: true)] private ?string $password = null;
#[ORM\Column(type: 'json')] private array $roles = ['ROLE_USER'];
#[ORM\Column(length: 120, nullable: true)] private ?string $displayName = null;
#[ORM\Column(type: 'datetime_immutable')] private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)] private ?\DateTimeImmutable $lastSeenAt = null;}The entity exposes one method per use case: linkOidcSubject, refreshDisplayInfo,
promoteToAdmin. Direct property writes from controllers are not allowed.
OIDC Lifecycle
Section titled “OIDC Lifecycle”The OIDC callback is handled by App\Identity\Infrastructure\AuthentikUserProvider.
On the first login the provider creates the user; on subsequent logins it refreshes
display_name, email and last_seen_at.
sequenceDiagram participant Browser participant Symfony as Symfony firewall participant Authentik participant DB as PostgreSQL users
Browser->>Symfony: GET /oidc/login Symfony-->>Browser: 302 to Authentik authorize URL Browser->>Authentik: GET authorize Authentik-->>Browser: 302 to /oidc/callback?code=... Browser->>Symfony: GET /oidc/callback?code=... Symfony->>Authentik: POST token (code, client_id, client_secret) Authentik-->>Symfony: tokens + id_token (sub, email, name, groups) Symfony->>DB: SELECT by oidc_sub alt user not found Symfony->>DB: INSERT new user with oidc_sub, email, display_name else user found Symfony->>DB: UPDATE display_name, email, last_seen_at end Symfony-->>Browser: 302 to / with session cookieThe Authentik client is configured with the redirect URI https://yourbox.site/oidc/callback,
PKCE enabled, refresh tokens enabled and the openid email profile groups scope.
Role Model
Section titled “Role Model”YourBOX has three roles enforced by Symfony voters:
ROLE_USERis granted automatically on first login. Required for upload, comment, vote and profile management.ROLE_MODERATORallows reviewing reports and soft deleting comments. Granted from the Moderation admin panel by aROLE_ADMIN.ROLE_ADMINallows the full EasyAdmin surface (videos, users, moderation). Granted in OpenBao by listing the user’s Authentiksubinkv/yourbox/admins, propagated to the entity through the external secrets sync.
The voter that decides on a video edit checks two things: the actor is the uploader,
or the actor has ROLE_MODERATOR and the request carries a valid X-Reason header
captured for the audit log.
CSRF And Session Defenses
Section titled “CSRF And Session Defenses”- Every state changing endpoint requires a CSRF token. Symfony Forms inject it
automatically; the few raw POST handlers under
Engagement(vote endpoints) callcsrf_token('vote_'.$id)in Twig andisCsrfTokenValid('vote_'.$id, ...)in PHP. - Session cookies are
Secure,HttpOnly,SameSite=Lax. The session id rotates on login and on privilege elevation. - The remember me cookie uses Symfony’s built in remember me handler with a 7 day
lifetime, signed with
APP_SECRET. The broken 2020 cookie format is gone. - The form firewall rate limits failed logins at 5 attempts per minute per IP through the Symfony rate limiter. In production, Authentik enforces the equivalent policy.
Account Recovery
Section titled “Account Recovery”In production, account recovery is delegated to Authentik. The “forgot password” flow
in the legacy tree (forget.php, modify_password.php, reset.php) is deleted as
part of the authentication migration step documented on the Symfony Migration page.
In development and on the VPS, the seeded admin and user accounts have known
credentials documented in .env.example. There is no password reset flow on
those environments because they are throwaway.
Audit Log
Section titled “Audit Log”Every privileged action (admin login, role change, video deletion, user ban) emits a
domain event consumed by the audit log writer in App\Moderation\Infrastructure\Audit.
The audit log writes to a dedicated audit_log PostgreSQL table with the schema
(id, occurred_at, actor_sub, actor_username, action, target_type, target_id, payload JSONB, ip_address). Records are append only; deletion is reserved to a
manual database operation under change control.
The audit log is shipped to Loki through Promtail and indexed in OpenSearch for cross service correlation as documented on the Observability page.
What This Page Does Not Cover
Section titled “What This Page Does Not Cover”- The Authentik tenant configuration itself, including authentication factors, group mappings and admin role definitions. That belongs to the homelab platform documentation.
- The OpenBao KV layout for application secrets. See the Secret Management page.
- The network policies that gate the OIDC traffic between the YourBOX pod and the Authentik pod. See the Kubernetes GitOps page.