Commit Graph

135 Commits

Author SHA1 Message Date
Claude
fc9c21915d feat(auth): enforce OpenID Connect issuer uniqueness across providers
Detect when two configured OIDC providers resolve to the same issuer URL
at startup and halt with a fatal error, preventing team sync data
corruption caused by ambiguous (external_id, issuer) matching.

Also adds duplicate issuer detection to the doctor service diagnostics
and comprehensive tests with mock OIDC discovery servers.
2026-03-30 22:41:50 +00:00
kolaente
83bac15841 feat: rename ServiceJWTSecret to ServiceSecret with deprecation (#2502) 2026-03-30 12:07:01 +02:00
kolaente
649043aceb test: add tests for OAuth 2.0 authorization flow
Add web tests covering the authorize endpoint, token exchange, PKCE
verification, single-use codes, and refresh token rotation. Add unit
tests for redirect URI validation and PKCE. Add E2E test for the full
browser-based authorization code flow with login redirect.

Extract setupApiUrl helper for E2E tests to avoid duplication.
2026-03-27 23:05:04 +00:00
kolaente
7827ff64b9 feat: add OAuth 2.0 token endpoint
Add POST /api/v1/oauth/token supporting authorization_code and
refresh_token grant types. Validates PKCE, exchanges codes for
JWT access tokens with refresh token rotation. Uses the shared
RefreshSession helper for the refresh grant.
2026-03-27 23:05:04 +00:00
kolaente
8b379b7466 feat: add OAuth 2.0 authorize endpoint
Add POST /api/v1/oauth/authorize behind auth middleware. Validates
OAuth parameters (response_type, redirect_uri, PKCE), fetches the
authenticated user, creates an authorization code, and returns it
as JSON for the frontend to handle the redirect.
2026-03-27 23:05:04 +00:00
kolaente
a6e7475153 feat: add OAuth client validation and PKCE verification
Add redirect URI validation that allowlists vikunja-* custom protocol
schemes, rejecting http/https and dangerous schemes like javascript:.
Add PKCE S256 verification following RFC 7636.
2026-03-27 23:05:04 +00:00
kolaente
7a258f67c7 refactor: extract shared RefreshSession helper
The cookie-based /user/token/refresh handler had session refresh logic
(lookup, expiry check, token rotation, user fetch, JWT generation)
that will be reused by the OAuth token endpoint. Extract it into
auth.RefreshSession() and rewrite RefreshToken to use it.
2026-03-27 23:05:04 +00:00
Claude
121fd3c9f1 feat: use openid provider name instead of generic "OIDC" in synced team names
Teams synced from OpenID Connect providers were always named with "(OIDC)"
suffix (e.g., "DevTeam (OIDC)"). This changes it to use the configured
provider name instead (e.g., "DevTeam (Keycloak)"), making it easier to
identify which provider a team came from when multiple OIDC providers are
configured. Existing team names will be updated automatically on next user
login.

https://claude.ai/code/session_012LXXPvYe6i27WTcha1PL7A
2026-03-24 12:30:06 +00:00
kolaente
fd452b9cb6 fix(auth): skip profile updates for disabled LDAP users
When a disabled/locked LDAP user authenticates, return early from
getOrCreateLdapUser without updating their profile info or syncing
avatar. The login handler already rejects them, but this avoids
unnecessary database writes.

Ref: GHSA-94xm-jj8x-3cr4
2026-03-23 16:37:26 +00:00
MidoriKurage
68a74416a4 fix(openid): Merge VikunjaGroups and ExtraSettingsLinks from userinfo
When `forceuserinfo: true`, `mergeClaims` discards `vikunja_groups`
and `extra_settings_links` claims fetched from the userinfo endpoint,
failing team sync for opaque tokens.

Fixes team sync for OIDC providers using opaque tokens.
2026-03-23 16:11:17 +00:00
kolaente
a7a8ae072a fix(auth): return correct error type for locked users in OIDC callback
Return ErrAccountLocked for locked users instead of ErrAccountDisabled.
Also skip profile updates and avatar sync for disabled/locked users
found during OIDC login — HandleCallback rejects the auth anyway.
2026-03-23 12:06:16 +00:00
kolaente
8409bdb120 refactor(user): export IsErrUserStatusError for use across packages
Make isErrUserStatusError public and replace all verbose
!IsErrAccountDisabled(err) && !IsErrAccountLocked(err) checks
with the shorter IsErrUserStatusError(err) call.
2026-03-23 12:06:16 +00:00
kolaente
22a4b6fbb8 fix(auth): reject disabled/locked users in OIDC callback 2026-03-23 12:06:16 +00:00
kolaente
ea4ba18def fix(user): handle status errors across the codebase, remove redundant checks 2026-03-23 12:06:16 +00:00
kolaente
530973c475 fix(auth): make SameSite=None conditional on HTTPS for refresh cookie
SameSite=None requires Secure=true per browser spec. When running over
plain HTTP (local dev, e2e tests), browsers reject or downgrade the
cookie, breaking session refresh. Fall back to SameSite=Lax for HTTP
while keeping SameSite=None for HTTPS (needed for the Electron desktop
app cross-origin scenario).
2026-03-03 10:41:19 +01:00
kolaente
28f98a7a96 fix(auth): use SameSite=None for refresh token cookie to fix desktop app
SameSite=Strict prevents the browser from sending the HttpOnly refresh
token cookie in cross-origin contexts like the Electron desktop app,
where the page runs on localhost but the API is on a remote host. This
caused sessions to expire quickly because refresh requests never
included the cookie.

SameSite=None allows cross-origin sending while HttpOnly still prevents
JavaScript from reading the cookie value (XSS protection).

Resolves #2309
2026-03-02 13:54:10 +01:00
kolaente
f3ac0574c0 fix(auth): use checked type assertions for all JWT claims 2026-02-25 13:01:00 +01:00
kolaente
b3d8a56364 fix: use caller's session in LDAP syncUserGroups to avoid nested transactions
syncUserGroups created its own db.NewSession() internally while being
called from AuthenticateUserInLDAP which already has an active session
with writes. In SQLite shared-cache mode this causes a lock conflict.

Pass the caller's session through instead, and add s.Commit() before
db.AssertExists calls in LDAP tests.
2026-02-25 11:03:02 +01:00
kolaente
49bba7f830 fix: eliminate nested database sessions to prevent table locks
Refactor functions that created their own sessions when called from
within existing transactions, which caused "database table is locked"
errors in SQLite's shared-cache mode.

Changes:
- Add files.CreateWithSession() to reuse caller's session
- Refactor DeleteBackgroundFileIfExists() to accept session parameter
- Add variadic session parameter to notifications.Notify() and
  Notifiable.ShouldNotify() interface
- Update all Notify callers (~17 sites) to pass their session through
- Use files.CreateWithSession in SaveBackgroundFile and NewAttachment
- Fix test code to commit sessions before assertions
2026-02-25 11:03:02 +01:00
kolaente
764d3569ce fix: close leaked database sessions
Add defer s.Close() to sessions that were never closed:
- auth.GetAuthFromClaims inline session
- models.deleteUsers cron function
- notifications.notify database insert
2026-02-25 11:03:02 +01:00
kolaente
c9c250fb1c fix: add missing Commit() to write callers
After NewSession() auto-begins a transaction, callers that perform
writes must explicitly call Commit() for changes to persist. Without
this, writes are silently rolled back when Close() is called.

Affected callers:
- user deletion notification cron
- caldav token generation/deletion
- token cleanup cron
- mark-all-notifications-read endpoint
- saved filter view cron
- project background delete
- typesense reindex
- export cleanup cron
- task last-updated listener
- saved filter view listener
- SSO team cleanup cron
- migration status start/finish
- background set/remove handlers
- orphaned task position cleanup
- file creation
2026-02-25 11:03:02 +01:00
kolaente
8ee069a2a3 feat: add session-based auth with refresh token rotation
- Login creates a server-side session and sets an HttpOnly refresh
  token cookie alongside the short-lived JWT
- POST /user/token/refresh exchanges the cookie for a new JWT and
  rotates the refresh token atomically
- POST /user/logout destroys the session and clears the cookie
- POST /user/token restricted to link share tokens only
- Session list (GET) and delete (DELETE) routes for /user/sessions
- All user sessions invalidated on password change and reset
- CORS configured to allow credentials for cross-origin cookies
- JWT 401 responses use structured error code 11 for client detection
- Refresh token cookie name constants annotated for gosec G101
2026-02-25 10:30:25 +01:00
Quiwy
6dbc108be8 feat(auth): allow LDAP authentication with anonymous bind (#2226)
As discussed on Matrix, Vikunja currently prevents users from using LDAP
authentication if the server allows anonymous binds (common in local
environments like YunoHost). The application would previously trigger a
`log.Fatal` if `AuthLdapBindDN` or `AuthLdapBindPassword` were left
empty in the configuration.

#### **How this fixes the problem:**

* **Validation:** Removed the strict requirement for Bind credentials in
`InitializeLDAPConnection`.
* **Connection Logic:** Updated `ConnectAndBindToLDAPDirectory` to
attempt an `UnauthenticatedBind` from the `go-ldap` library when no
credentials are provided.
* **Safety:** If a Bind DN is provided, the behavior remains unchanged
(authenticated bind).

#### **Testing:**

* Tested manually on a **YunoHost** instance by replacing the binary.
* Confirmed that Vikunja now successfully starts and authenticates users
via the local LDAP (localhost) without requiring a service account.
* Added a basic unit test in `pkg/modules/auth/ldap/ldap_test.go` to
ensure the initialization logic doesn't crash with empty credentials.

*Note: This is my first contribution to a Go project (assisted by an LLM
for syntax). Feedback on code style is more than welcome!*
2026-02-17 22:24:35 +01:00
kolaente
e90cb2631d fix(auth): remove unnecessary fields from JWT token payloads
Remove email, name, emailRemindersEnabled, and isLocalUser from user JWT
claims, and isLocalUser from link share JWT claims. These fields are never
used from the token - the backend always fetches the full user from the
database by ID, and the frontend fetches user data from the /user API
endpoint immediately after login.

Also simplify GetUserFromClaims to only extract id and username, and
remove the now-unnecessary email override in the frontend's
refreshUserInfo.
2026-02-08 21:30:07 +01:00
renovate[bot]
9a61453e86 fix(deps): update module github.com/labstack/echo/v4 to v5 (#2131)
Closes https://github.com/go-vikunja/vikunja/pull/2133

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2026-01-24 20:38:32 +01:00
kolaente
4df8da549e fix(auth): scope query binding
Resolves https://github.com/go-vikunja/vikunja/issues/2146
2026-01-24 17:51:35 +01:00
kolaente
39b4568bc5 refactor: centralize HTTP error handling (#2062)
This changes the error handling to a centralized HTTP error handler in `pkg/routes/error_handler.go` that converts all error types to proper HTTP responses. This simplifies the overall error handling because http handler now only need to return the error instead of calling HandleHTTPError as previously.
It also removes the duplication between handling errors with and without Sentry.

🐰 Hop along, dear errors, no more wrapping today!
We've centralized handlers in a shiny new way,
From scattered to unified, the code flows so clean,
ValidationHTTPError marshals JSON supreme!
Direct propagation hops forward with glee,
A refactor so grand—what a sight to see! 🎉
2026-01-08 10:02:59 +00:00
kolaente
c6fe4c1a6e fix(auth): retry up to three times when an auth provider cannot be reached
Resolves https://github.com/go-vikunja/vikunja/issues/2050
2026-01-05 21:50:40 +01:00
kolaente
541a38456e chore(deps): update golangci-lint to 2.6.0 (#1737) 2025-10-31 17:28:52 +00:00
Copilot
c7a26d81fe fix(auth): do not panic with invalid openid provider configuration (#1354) 2025-08-31 07:17:50 +00:00
Copilot
5ca637a7e6 feat(auth): add oauth require availability configuration on startup (#1358) 2025-08-30 22:15:20 +00:00
kolaente
a81a3ee0e5 feat!: rename right to permission (#1277) 2025-08-13 11:05:05 +02:00
kolaente
da0f6fb366 feat(auth): allow passing custom settings links to user account via openid claims 2025-08-03 13:25:32 +02:00
kolaente
de917467cb fix(openid): manually fetch providers
Partially reverts fcdcdcf46a
Resolves https://github.com/go-vikunja/vikunja/issues/1165
2025-07-28 11:40:09 +02:00
kolaente
bbd3567e43 chore: add debug logging around provider failure
https://github.com/go-vikunja/vikunja/issues/1165
2025-07-24 16:00:03 +02:00
kolaente
7243a10fb2 fix(openid): check different provider types
Related to https://github.com/go-vikunja/vikunja/issues/1165
2025-07-23 15:40:51 +02:00
kolaente
2b497e6265 fix: pass pointer when fetching provider
Resolves https://github.com/go-vikunja/vikunja/issues/1165
2025-07-23 11:09:09 +02:00
kolaente
ad0cf7a13c fix: improve ldap sanitization (#1155) 2025-07-21 21:06:38 +00:00
kolaente
ca83ad1f98 feat: move to slog for logging 2025-07-21 18:15:39 +02:00
kolaente
566657c54a fix: correctly return cached provider 2025-07-18 18:38:12 +02:00
kolaente
fcdcdcf46a feat: use keyvalue.Remember where it makes sense 2025-07-17 16:19:13 +02:00
Dominik Pschenitschni
342bbd6192 fix: correct comments 2025-07-02 17:46:21 +02:00
kolaente
0ecbd9e1a3 feat(user): add avatar cache flushing (#1041) 2025-06-27 14:01:43 +02:00
Weijie Zhao
a214d68a44 feat(auth): sync avatar from OpenID providers (#821) 2025-06-16 15:59:31 +02:00
kolaente
6671ce38a8 chore: rename API test suites (#938) 2025-06-13 08:23:17 +00:00
Dominik Pschenitschni
296577a875 fix: correct license header references (#882)
See originals:
- https://www.gnu.org/licenses/agpl-3.0.txt
- https://www.gnu.org/licenses/gpl-3.0.txt
2025-06-10 12:18:38 +02:00
Weijie Zhao
00c4148f05 feat(auth): add ForceUserInfo option to OpenID provider (#797)
Problem:

When using Casdoor as an OpenID provider, there's an inconsistency between the user information in the JWT token and the UserInfo endpoint. The token contains the user's unique ID in the `name` field, while the UserInfo endpoint correctly returns the user's display name.

Solution:

This PR adds a new `ForceUserInfo` option to the OpenID provider configuration. When enabled, it forces the use of the UserInfo endpoint to retrieve user information instead of relying on claims from the ID token.

Impact:

- Default behavior remains unchanged (backward compatible)
- New option allows administrators to force using UserInfo endpoint data
- Particularly useful for providers like Casdoor that don't fully comply with OIDC standards

Related:

I've opened an issue in the Casdoor repository (https://github.com/casdoor/casdoor/issues/3806) to discuss the root cause. However, changing Casdoor's token structure might cause significant compatibility issues for existing integrations, so it's unclear if this can be fixed at the provider level. This PR provides a workaround in Vikunja that doesn't affect existing functionality.
2025-05-20 08:06:34 +00:00
kolaente
6847f44058 chore(openid): add more debug logging when retrieving token 2025-03-29 19:24:07 +01:00
kolaente
4ea3c01b5f chore: add more debug logging when returning error 2025-03-29 18:21:34 +01:00
kolaente
5a93379d81 fix(ldap): update user name and email during login 2025-03-20 17:24:00 +01:00