Make OAuth2 access token lifetime configurable (currently hardcoded to 3600s)
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 32
- Forks
- 24
- Avg merge
- 1d 32m
- Merged PRs (30d)
- 7
Description
Problem
The OAuth2 access token lifetime is hardcoded to one hour:
// lib/Db/AccessToken.php
public const EXPIRATION_TIME = 3600;
For deployments where desktop/mobile sync clients are the primary consumers, one hour is aggressive. In theory the refresh token flow makes expiry invisible to the user. In practice the desktop client hits an expired access token precisely in the moments when the silent refresh cannot happen: laptop resume after a weekend, docking/undocking, switching networks or locations during the day. In those cases the client falls back
to a full interactive re-authorization in the browser. This has come up before on Central, e.g. https://central.owncloud.org/t/oauth-valid-token-time/31794
With enforced 2FA this becomes genuinely unmaintainable. The browser autofills the password, but the TOTP code always has to be typed by hand. A user with several accounts configured in the desktop client (5 accounts is a realistic case for admins and multi-tenant users) starts every Monday morning with five separate 2FA challenges, and repeats some of them after every location change. With a 1-hour lifetime these
expiry windows are hit constantly; a longer lifetime (e.g. days) makes them a non-event, because after a resume the client almost always still holds a valid token.
Since there is no configuration option, admins who need a different value patch lib/Db/AccessToken.php directly. Any such patch is destroyed by every upgrade and, since the code integrity checker covers the oauth2 app, produces a permanent INVALID_HASH warning in the admin panel. That is the worst of both worlds: the setting effectively is tunable in the field, but only in a way that fights the platform's own tamper detection.
Proposal
Add a config.php system value, keeping the current default so nothing changes for existing installations:
'oauth2.access_token_expire_time' => 3600,
Sketch of the change:
--- a/lib/Db/AccessToken.php
+++ b/lib/Db/AccessToken.php
@@
- /**
- * Resets the expiry time to EXPIRATION_TIME seconds from now.
- */
- public function resetExpires() {
- $this->setExpires(\time() + self::EXPIRATION_TIME);
+ /**
+ * Resets the expiry time to $expirationTime (default EXPIRATION_TIME) seconds from now.
+ */
+ public function resetExpires(?int $expirationTime = null) {
+ $this->setExpires(\time() + ($expirationTime ?? self::EXPIRATION_TIME));
}
Then inject IConfig into OAuthApiController and PageController and resolve the value once per request:
$expiresIn = \max(60, (int)$this->config->getSystemValue(
'oauth2.access_token_expire_time',
AccessToken::EXPIRATION_TIME
));
$accessToken->resetExpires($expiresIn);
The same $expiresIn value must be reported to clients in both places that currently reference the constant, so expires_in stays truthful:
lib/Controller/OAuthApiController.php(token response, currently line ~280)lib/Controller/PageController.php(implicit flow fragment, currently line ~300)
AccessToken::EXPIRATION_TIME stays as the default, so there is no BC break for external code referencing the constant. AuthorizationCode::EXPIRATION_TIME (600s) is intentionally left out of scope — auth codes are short-lived by design and there is no operational reason to tune them.
Risk analysis
Why this is safe to expose as a config option:
- Access tokens in this app are opaque, DB-backed values (
oc_oauth2_access_tokens), validated against the database on every request. They are not self-contained signed tokens. Revocation (admin or user removing an authorization, disabling a user) takes effect immediately regardless of the configured lifetime. A longer TTL does not create an unrevocable credential. - Refresh tokens in this app do not expire at all (see #288). In any scenario where an attacker obtains a database dump or a client device, they already hold a non-expiring refresh token plus the (publicly known) client credentials of the official apps. The access token TTL is therefore not the primary security control in this design — the marginal exposure added by a longer access token is small compared to what a leaked refresh token already grants.
- The remaining scenario where TTL matters is a single access token intercepted in transit without the refresh token — only possible with broken TLS. Admins who raise the value consciously accept a longer replay window in that (narrow) case; the shipped default stays 3600.
- A lower bound (
max(60, ...)) protects against accidental misconfiguration (0/negative values would otherwise mint pre-expired or never-usable tokens).
Note the interaction with 2FA: a short access token lifetime does not add a second factor to anything — the silent refresh never involves 2FA. Its practical effect in a 2FA deployment is only to increase the frequency of interactive re-authorizations after refresh failures, which trains users to rush through 2FA prompts. A sane, configurable lifetime arguably improves the security posture there.
The documentation entry for the new key should state this tradeoff explicitly and recommend keeping the default unless frequent re-authorization is a real operational problem.
Environment
Observed on ownCloud 10.16.3 (oauth2 app bundled with core), PHP 7.4, desktop client with multiple accounts and TOTP 2FA enforced.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/Db/AccessToken.php, then inspect the token response in lib/Controller/OAuthApiController.php and the implicit flow in lib/Controller/PageController.php. Trace how each currently uses AccessToken::EXPIRATION_TIME and how IConfig is injected. Done means the config.php value controls token expiry in both flows, expires_in remains accurate, the 3600-second default is preserved, and the new setting is documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100