Skip to content

feat: OAuth 2.0 authorization server for Flutter app#2308

Draft
kolaente wants to merge 15 commits intomainfrom
feat-oauth-server
Draft

feat: OAuth 2.0 authorization server for Flutter app#2308
kolaente wants to merge 15 commits intomainfrom
feat-oauth-server

Conversation

@kolaente
Copy link
Member

Summary

This PR implements an OAuth 2.0 authorization server to enable the Flutter app to authenticate using the authorization code flow with PKCE.

Changes

Backend

  • OAuthCode model (pkg/models/oauth_codes.go) - Stores authorization codes with PKCE challenge, user ID, and expiry
  • Database migration (pkg/migration/20260226172819.go) - Creates oauth_codes table
  • OAuth error types (pkg/models/error.go) - Added 7 error types (17001-17007) for OAuth-specific errors
  • OAuth2 server package (pkg/modules/auth/oauth2server/):
    • client.go - Client validation (Flutter client ID: vikunja-flutter, redirect URI: vikunja://callback)
    • pkce.go - PKCE S256 verification
    • authorize.go - Authorization endpoint handler (GET /api/v1/oauth/authorize)
    • token.go - Token endpoint handler (POST /api/v1/oauth/token) with authorization_code and refresh_token grant types
  • Route registration (pkg/routes/routes.go) - Registered OAuth endpoints

Frontend

  • Login.vue and OpenIdAuth.vue - Handle OAuth redirect parameter after successful login

Tests

  • Unit tests for client validation and PKCE verification
  • Web tests (pkg/webtests/oauth2_test.go) - 11 integration test cases covering the full OAuth flow

OAuth Flow

  1. Flutter app opens /api/v1/oauth/authorize with client_id, redirect_uri, response_type, code_challenge, code_challenge_method
  2. If not logged in, redirects to login page with return URL
  3. After login, redirects back to authorize endpoint
  4. Authorize endpoint creates code and redirects to vikunja://callback with code
  5. Flutter app exchanges code for tokens via POST /api/v1/oauth/token
  6. Refresh tokens can be exchanged for new access/refresh token pairs

Security Features

  • PKCE (S256) required for all authorization requests
  • Single-use authorization codes (deleted on use)
  • 10-minute code expiry
  • Refresh token rotation
  • Same-origin validation for OAuth redirects in frontend

The refresh token cookie was path-scoped to /api/v1/user/token/refresh,
which prevented it from being sent on requests to /api/v1/oauth/authorize.
This broke the browser-based OAuth flow: after login, when Login.vue
redirects to the authorize endpoint via window.location.href, the server
could not detect the user's session because the cookie wasn't included.

Widen the path to /api/v1/ so the cookie is sent on all API requests.
The cookie remains HttpOnly and SameSite=Strict, so the security posture
is unchanged.
Covers the full browser flow that backend tests cannot exercise:
unauthenticated user → login redirect → login via UI → Login.vue
reads the redirect query param and navigates back to the authorize
endpoint → authorization code issued → token exchange.

Uses page.route() to proxy the authorize request from the frontend
origin to the real API server with the JWT, since the E2E test runs
the API and frontend on separate ports (in production they share an
origin).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant