OAuth 2.0 Authorization Server

This feature is available from Vikunja 2.3.0 onwards (or unstable builds).

Vikunja can act as an OAuth 2.0 Authorization Server, implementing the Authorization Code flow with mandatory PKCE (Proof Key for Code Exchange, S256 only). This allows native apps and third-party integrations to authenticate users and obtain access tokens without handling user credentials directly.

Key characteristics:

  • PKCE is mandatory (S256 method only).
  • No client registration is required. The client_id can be any string chosen by the integrator; it must be consistent between the authorization and token exchange steps.
  • Redirect URIs must use a custom scheme starting with vikunja- (for example, vikunja-flutter://callback). Standard http:// and https:// URLs are not supported.
  • All request and response bodies use JSON. Form-encoded requests are not supported.
  • There is no consent screen. Authorization is granted automatically once the user is authenticated.

Relevant specifications#

Endpoints#

EndpointMethodAuthenticationPurpose
/oauth/authorizeBrowser navigationNone (frontend route)User-facing authorization page
/api/v1/oauth/authorizePOSTJWT BearerCreates an authorization code
/api/v1/oauth/tokenPOSTNoneExchanges a code for tokens or refreshes tokens

Authorization flow#

The complete flow consists of four steps:

1. Redirect the user to the authorization page#

Open a browser or webview to the Vikunja instance’s authorization endpoint with the following query parameters:

https://<vikunja-host>/oauth/authorize
  ?response_type=code
  &client_id=<your-client-id>
  &redirect_uri=vikunja-flutter://callback
  &code_challenge=<challenge>
  &code_challenge_method=S256
  &state=<random-string>
ParameterRequiredDescription
response_typeYesMust be code.
client_idYesAny string that identifies your application. Must match at token exchange.
redirect_uriYesWhere Vikunja redirects after authorization. Must use a vikunja- scheme.
code_challengeYesBase64url-encoded SHA-256 hash of the code_verifier (see RFC 7636 Section 4.2).
code_challenge_methodYesMust be S256.
stateRecommendedAn opaque value to prevent CSRF. Your app should verify it when receiving the callback.

Vikunja’s frontend handles user login if the user is not already authenticated.

2. Receive the authorization code#

After the user authenticates, Vikunja redirects to your redirect_uri with the authorization code and state:

vikunja-flutter://callback?code=<authorization-code>&state=<state>

Your application should verify that the returned state matches the one you sent.

The authorization code is single-use and expires after 10 minutes.

3. Exchange the code for tokens#

Make a POST request to the token endpoint with a JSON body:

curl -X POST https://<vikunja-host>/api/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "<authorization-code>",
    "client_id": "<your-client-id>",
    "redirect_uri": "vikunja-flutter://callback",
    "code_verifier": "<original-code-verifier>"
  }'
FieldRequiredDescription
grant_typeYesMust be authorization_code.
codeYesThe authorization code received in step 2.
client_idYesMust match the client_id used in step 1.
redirect_uriYesMust match the redirect_uri used in step 1.
code_verifierYesThe original random string used to generate the code_challenge.

Response:

{
  "access_token": "<jwt-access-token>",
  "token_type": "bearer",
  "expires_in": 600,
  "refresh_token": "<refresh-token>"
}

The expires_in value is configured server-side via the service.jwtttlshort setting (default: 600 seconds).

4. Refresh the access token#

When the access token expires, use the refresh token to obtain a new one:

curl -X POST https://<vikunja-host>/api/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "<refresh-token>"
  }'

Response:

The response has the same format as the token exchange response, including a new refresh_token. Refresh tokens are rotated on each use — the previous refresh token becomes invalid after a successful refresh.

Generating the PKCE code challenge#

If your OAuth library does not handle PKCE automatically, here is how to generate the values:

  1. Create a code_verifier: a cryptographically random string between 43 and 128 characters, using characters [A-Z] / [a-z] / [0-9] / - / . / _ / ~.
  2. Compute the code_challenge: take the SHA-256 hash of the code_verifier, then base64url-encode it (without padding).

See RFC 7636 Section 4.1 and Section 4.2 for the full specification.

Error codes#

All errors are returned as HTTP 400 responses with a JSON body containing a code field. See the OAuth section of the error codes reference for the full list.