Important: anti-enumeration is active only if SMTP is defined, welcome email can be used
otherwise it is a classic registration with ok or conflic if account alrady exists
Backend
- RegisterDto — username and password both become Option<String> with #[serde(default)] so JSON can omit them entirely.
- AuthApplicationService::register — username uniqueness check skipped when None (multiple NULLs OK under the UNIQUE index); password hashing skipped when None; User::new called with the actual Options instead of forcing Some(...).
- auth_handler::register — branches on dto.password.is_none(). With password → existing 201 + UserDto. Without → triggers MagicLinkInviteService::send_login_link(&email) best-effort, then returns 200 + {"message": "Check your email…"}. The
OIDC-mode-disables-password-registration gate now only fires for the password path (email-only signup is still allowed even in OIDC-only mode, because it doesn't store a password).
- magic_link_handler::redirect_target — new 3-way decision tree:
- Resource target (folder invitation) → /#/files/folder/{id} (existing)
- NULL resource + is_external = false → /#/files (the welcome path for new internal users — they have a home folder)
- NULL resource + is_external = true → /#/sharedwithme (the existing external-user landing)
Tests
- New tests/api/registration.hurl with 9 requests covering: classic (with-password) register → 201 + UserDto, email-only register → 200 + uniform message + welcome magic-link captured, redemption → 302 to /#/files + cookies set, profile read → username
absent + is_external: false, resend magic-link works (eligible while passwordless), cleanup deletes both new users.
- Wired into tests/api/run.sh right after auth_login.hurl.
Plan additions
- auth-simplification.md gained PR 22 at the bottom of the PR sequence — device-bound magic-link redemption via challenge cookie + asymmetric TTLs (login: 10 min, invitation: 24 h). Full design recap, schema migration, config knobs
(OXICLOUD_MAGIC_LINK_LOGIN_TTL_MINUTES / _INVITE_TTL_HOURS), and Hurl coverage outline are in the plan. Slots in before PR 21's docs so the architecture page describes the final state from the start.
Checks — cargo fmt, cargo clippy --all-features --all-targets -- -D warnings, cargo test --lib (297 passed), biome, stylelint, tsc, full Hurl suite (16 files) all green.
username: now optional, if defined 2..64 chars
password: now optional (no mode __NO_PASSWORD...__)
oidc: now optional
important: if need Nextcloud, username must be defined
- add possibility to grant an external user.
- route /api/users/{id} added (rate limited for security)
- security: start route limitation for external users
ex: they must not browse /api/users/{id} nor addressbook
reflect OIDC
schema migration, User entity additions, and three defense-in-depth gaps closed, with all 280 unit tests and 13 Hurl files green
infrastructure (lettre + EmailSender port).
- Migration migrations/20260612000003_users_username_email_login.sql — adds nullable given_name/family_name columns to auth.users.
- User entity (src/domain/entities/user.rs) — has_login_credential() placeholder-check encapsulation, set_username revalidating setter, given/family-name fields + getters/setters, validate_username widened 32→254 and now accepts email shape.
from_data_full extended with two new params; all 7 callsites in user_pg_repository.rs updated.
- Schema-side legacy guards (src/application/services/auth_application_service.rs) — bumped the duplicated 32-char check in setup_create_admin and admin_create_user to 254 to match.
- Gap #1 (subject_group_service.rs) — add_member now rejects external candidates with an audit-logged AccessDenied. Service gained an Arc<UserPgRepository> field, wired through DI. New integration test test_external_user_cannot_be_added_as_member.
- Gap #2 (user_repository.rs + auth_ports.rs + user_pg_repository.rs) — list_users/search_users gained an include_external: bool param defaulting effectively to false everywhere internal-user-facing. auth_application_service exposes a new
list_users_including_external for the admin surface.
- Gap #3 (pg_acl_engine.rs) — expand_user now SELECTs is_external and skips INTERNAL_GROUP_ID for externals; defaults to is_external=true on missing user to fail closed.
- AuthzCacheLifecycleHook — invalidates the user_groups_cache Moka entry on logout/delete.
- SessionRevocationLifecycleHook — explicit per-session firing of on_user_logout (currently per-call); session revocation inside the user-delete transaction.
- DeletionMode-driven policy in HomeFolderLifecycleHook::on_user_deleted (trash vs hard-delete based on AdminDelete / GdprPurge).
- Refactor delete_user_admin to expose a transaction handle so on_user_deleted can abort atomically.
Security: session hardening
Refresh token rotation with theft detection (family_id)
- Added family_id column to auth.sessions (migration 20260507000000_session_family.sql) grouping all tokens issued from the same login into a family
- On refresh, the new session inherits the parent's family_id
- If a revoked token is replayed (indicates the token was stolen after rotation), the entire family is immediately invalidated and a warning is logged — forcing re-authentication on all devices
SameSite=Strict on refresh cookie
- Access cookie stays SameSite=Lax (needed for top-level navigation)
- Refresh cookie upgraded to SameSite=Strict — it is only ever used for explicit POST to /api/auth/refresh, never via cross-site navigation
Refresh token TTL: 30 days → 7 days
- With rotation, active sessions auto-renew and effectively never expire
- Inactive sessions expire after 7 days instead of 30, reducing the theft window
When OIDC providers (e.g. Keycloak) use email addresses as usernames or
when claims.sub contains @ or other invalid characters, the username
padding and collision-suffix logic could introduce invalid characters.
The fix filters claims.sub through the same allowed-character filter
before using it in username construction.
FixesDioCrafts/OxiCloud#259
The admin_create_user method was using hardcoded quota values (100GB for
admin, 1GB for user) instead of the capped_quota method that checks
available disk space. This could result in setting a quota higher than
the actual available disk space.
Fixes#92
Fix all clippy lints (collapsible if, clone on Copy, needless borrow,
redundant bindings, unused params) and apply rustfmt across the codebase.
Update test mocks to match Uuid-based trait signatures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- JWT secret auto-generates and persists to <STORAGE_PATH>/.jwt_secret
- Remove setup token: first admin setup is open until system initialized
- Fix schema.sql: move CREATE EXTENSION pg_trgm/ltree to top
- Update login UI and auth.js to remove setup token fields
Implement a complete Nextcloud client compatibility layer so that
Nextcloud desktop/mobile sync clients can connect to OxiCloud.
Key additions:
- Login Flow v2 (device auth) with OIDC bridge support
- WebDAV handler compatible with Nextcloud clients (PROPFIND, GET,
PUT, DELETE, MKCOL, MOVE, COPY, HEAD, PROPPATCH)
- OCS API endpoints (user info, capabilities, notifications stubs,
sharees, unified search)
- Basic Auth middleware with app password verification, account
lockout integration, and blake3-keyed auth cache
- App password management: create, list, revoke via both native
API (JWT-authenticated profile page) and Nextcloud OCS endpoints
- Nextcloud file ID mapping (oc:fileid) with persistent DB storage
- Chunked upload support (Nextcloud v2 chunking protocol)
- Trashbin WebDAV interface
- Avatar (SVG placeholder) and preview (redirect) handlers
- User profile page with app password management UI
- URL user validation on all DAV routes (403 on mismatch)
- Database schema for app_passwords and nextcloud_object_ids tables
All services are behind a `nextcloud.enabled` config flag and
cleanly separated under src/interfaces/nextcloud/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add type aliases (FileRow, FolderRow, FolderRowPaginated, FolderRowOptUser) to reduce type complexity
- Simplify redundant closures in app_password_handler and webdav_handler
- Remove needless borrow in auth_handler
- Collapse nested if/let chains in login_lockout, webdav_lock, auth, rate_limit
- Box LockEntry in acquire() Err variant to fix large enum variant warning
- Rename DeviceCodeStatus::from_str to parse to avoid should_implement_trait lint
- Add #[allow(clippy::too_many_arguments)] and #[allow(clippy::result_unit_err)] where appropriate
- Convert integration_tests from cargo feature to custom cfg attribute
- Add check-cfg lint config in Cargo.toml for integration_tests cfg
Remove async-trait dependency and use native Rust async fn in traits.
Replace Arc<dyn Trait> with Arc<ConcreteType> throughout the codebase
to enable monomorphization and eliminate dynamic dispatch overhead.
Key changes:
- Remove write-behind cache (no implementation existed)
- Fix should_transcode static method call
- Use ContactStorageAdapter directly instead of dyn AddressBookUseCase
- Clean up unused trait imports across services and DI
https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
- Parse email_verified from ID token and UserInfo endpoint
- Reject OIDC login if email is present and not verified
- Only applies when email is in OIDC claims (not required otherwise)
- Add auth_provider field to UserDto, derived from oidc_provider
("local" for password users, provider name for OIDC users)
- Block change_password() for OIDC users with clear error message
- Block admin_reset_password() for OIDC users
Fixes#122, Fixes#123
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run `cargo fmt` across all Rust source files to enforce consistent
formatting (import ordering, line wrapping, match arm braces).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix 'folder_id is required' upload error by adding create_home_folder
through the full hexagonal architecture (trait, service, repository, auth)
- Fix double upload issue with _isUploading concurrency guard
- Fix Share context menu doing nothing (ID collision between sharedView
and main share dialog resolved with sv- prefix)
- Fix Compartidos tab duplicate headers and broken layout
- Add missing .shared-dialog CSS with dark mode support
- Fix dark mode white backgrounds on empty-state, shared-filters,
trash-actions, action-btn, and header
- Fix i18n key mismatches in sharedView
- Bump version to 0.4.1
Closes#120
Standardize code formatting across all 173 Rust source files
using rustfmt. No functional changes - purely cosmetic.
This establishes a consistent code style baseline for the
project going forward.
Previously the admin quota was hardcoded to 100 GB and the regular
user quota to 1 GB, regardless of actual disk capacity. On systems
with less than 100 GB free this produced misleading quota values.
Added the fs2 crate to query available disk space on the storage
filesystem. A new capped_quota() helper now returns
min(default_quota, available_disk_space) when assigning quotas
during user registration, admin setup, and OIDC provisioning.
get_authorize_url() was synchronous and fell back to constructing
{issuer}/authorize when the discovery cache was empty. This produced
incorrect URLs for providers like Keycloak whose authorization
endpoint is {issuer}/protocol/openid-connect/auth.
Made get_authorize_url() async so it can call get_discovery() to
fetch the real authorization_endpoint from .well-known/openid-configuration
before the first redirect. The discovery document is cached after the
initial fetch.
Backend:
- POST /api/admin/users — admin-only user creation endpoint
- username & password required, email optional (auto-generated placeholder)
- role, quota_bytes, active all configurable
- creates personal folder automatically
- PUT /api/admin/users/{id}/password — admin password reset
- GET/PUT /api/admin/settings/registration — toggle public registration
- Supports env var OXICLOUD_DISABLE_REGISTRATION override
- Blocks POST /api/auth/register when disabled
- AdminCreateUserDto, AdminResetPasswordDto added to settings DTOs
- registration_enabled field added to DashboardStatsDto
Frontend (admin.html):
- 'Create User' button in Users tab with full modal form
(username, password, email, role, quota)
- 'Reset Password' button per user in actions column
- 'Allow public self-registration' toggle in Dashboard > System
with warning banner when disabled
Closes#85
Three bugs caused 403 errors when creating the first admin on fresh
Docker deployments (Unraid, Komodo):
1. db.rs: Schema application failures were silently swallowed. The app
started with no tables, causing all auth queries to fail. Now the
startup aborts if schema cannot be applied, with a fallback
statement-by-statement executor that handles dollar-quoted blocks.
Retries increased to 5 with 2s intervals.
2. auth_application_service.rs: count_admin_users() used fragile string
matching (contains "does not exist")) on multi-layer wrapped errors.
count_all_users() rejected admin creation on any DB error. Both now
allow admin creation on any error for bootstrap scenarios.
3. auth_handler.rs: Redundant 60-line handler-level admin detection
duplicated service-layer logic and generated noisy ERROR logs on
fresh installs. Removed entirely - service layer handles it all.
Closes#81
- Auto-apply schema.sql when database tables don't exist (embedded in binary)
- Handle missing tables gracefully during admin registration (treat as fresh install)
- Fix docker-compose depends_on to wait for postgres healthcheck
- Rename personal folder from 'Mi Carpeta' to 'My Folder' with backward compat
- Translate remaining Spanish messages to English
Security fixes for OIDC authentication flow:
1. CSRF state validation (High): State nonce is now stored server-side
and validated on callback (single-use, 600s TTL)
2. PKCE S256 (Medium): code_challenge/code_verifier pair generated per
RFC 9126, sent in authorize URL and token exchange
3. Nonce in ID token (Medium): Random nonce included in authorize URL,
verified against ID token claims to prevent token replay
4. Secure token delivery (Medium): Tokens no longer in URL fragments.
One-time exchange code redirected to frontend, tokens retrieved via
POST /api/auth/oidc/exchange endpoint (60s TTL, single-use)
5. Registration guard (Low): POST /api/auth/register returns 403 when
disable_password_login is active in OIDC-only mode