Skip to content

Add per-client token TTL and expiry support for client credentials - #77

Merged
roborourke merged 7 commits into
WP-API:mainfrom
abhishek-kaushik:token-expiry
Sep 23, 2026
Merged

roborourke merged 7 commits into
WP-API:mainfrom
abhishek-kaushik:token-expiry

Conversation

@abhishek-kaushik

@abhishek-kaushik abhishek-kaushik commented May 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Per-client TTL setting — Each OAuth client can configure a token TTL (in seconds) via the admin UI. Existing clients default to no TTL (tokens never expire), preserving backwards compatibility.
  • Expiry stored on the token — Access_Token::create_for_client() reads the TTL from the client and sets an expires timestamp on the token at creation time.
  • Expired token rejection — The authentication layer rejects expired tokens with a 401 Unauthorized.
  • expires_in in token response — The /oauth2/access_token endpoint includes expires_in in the response only when the token actually has an expiry.

Backwards Compatibility

Existing clients have no TTL stored — tokens issued to them will not expire. New clients opt in by setting a TTL value in the admin UI.

@abhishek-kaushik
abhishek-kaushik marked this pull request as ready for review May 12, 2026 09:41
@abhishek-kaushik abhishek-kaushik changed the title Token expiry Add per-client token TTL and expiry support for client credentials May 12, 2026
@joehoyle

Copy link
Copy Markdown
Member

@abhishek-kaushik could you give me a review of #75 (I added you to this repo) so we can merge tests, and then add test with this PR

@abhishek-kaushik

Copy link
Copy Markdown
Collaborator Author

@abhishek-kaushik could you give me a review of #75 (I added you to this repo) so we can merge tests, and then add test with this PR

sure @joehoyle , can do it

@joehoyle

Copy link
Copy Markdown
Member

Ok tests per merged, you should be able to add tests here now

@joehoyle

joehoyle commented Sep 1, 2026

Copy link
Copy Markdown
Member

@abhishek-kaushik ping on the above ^

roborourke and others added 3 commits September 22, 2026 17:35
The test harness from WP-API#75 and the PHP 7.4+ baseline only exist on main,
so the branch has to catch up before tests can be written against it.
The only conflict was the use statement block in the authentication
namespace, where both sides added an import.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the four parts of the feature: the TTL stored on the client, the
expiry stamped on a client credentials token, the authentication layer
rejecting an expired token, and expires_in in the token response.

Each case was checked by mutation: disabling any one of the four code
paths in turn fails at least one of these tests.

Two edges are pinned deliberately. A token at exactly its expiry
timestamp counts as expired, because the check is `>=`. An expired token
still resolves from get_by_id(), so the authentication layer can tell an
expired token apart from an unknown one and return the right error.

test-admin.php is new — validate_parameters() had no coverage, and the
TTL field is validated there.

The existing test_is_valid_always_true() is renamed: is_valid() is no
longer unconditional now that it defers to is_expired().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PHPCS job fails on this branch: the TTL comparisons are not Yoda
conditions, and reordering the Access_Token constants left the equals
signs unaligned. Both are WPCS rules the rest of the plugin follows.

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@roborourke

Copy link
Copy Markdown
Collaborator

@joehoyle I've added the tests. I also merged main in first, because the test harness from #75 wasn't on this branch and the PR had a conflict. It merges cleanly now.

34 new tests, in four places:

  • test-client.php — the TTL is stored on the client, cast to an int, and cleared when the field is emptied.
  • test-access-token.php — a client credentials token gets an expiry when the client has a TTL, gets none when it doesn't, and the expiry survives a reload. User tokens never expire.
  • test-authentication.php — an expired token is rejected with oauth2.authentication.token_expired and a 401, and a token inside its TTL still authenticates.
  • test-admin.php (new file) — validate_parameters() handles the TTL field. It had no coverage before.
  • test-token-endpoint.php — expires_in appears only when the token actually expires.

I checked the tests are worth having by breaking each of the four code paths in turn. Every one of them fails at least one test.

Two edges are pinned on purpose. A token at exactly its expiry second counts as expired, since the check is >=. An expired token still resolves from get_by_id(), so the auth layer can tell "expired" from "unknown" and return the right error.

Last commit fixes PHPCS on the TTL code (Yoda conditions, and the constant alignment that the reordering in Access_Token broke). PHPCS was failing before that; it passes now, as do 221 tests on single site and multisite.

One thing I left alone: validate_parameters() turns a non-numeric TTL like abc into 0, which means "expire immediately" rather than an error. The field is type=number, so it only bites a hand-crafted POST. Worth a follow-up?

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

TTL validation currently accepts malformed input by partially or implicitly casting it to an integer.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds configurable per-client TTLs for client-credentials tokens while preserving non-expiring legacy behavior.

Changes:

  • Stores client TTL settings and token expiry timestamps.
  • Rejects expired tokens and returns conditional expires_in.
  • Adds admin controls and expiry-focused tests.
File Description
inc/​admin/​namespace.php Adds TTL validation and admin UI.
inc/​authentication/​namespace.php Rejects expired access tokens.
inc/​class-client.php Persists per-client TTL settings.
inc/​endpoints/​class-token.php Returns expires_in when applicable.
inc/​tokens/​class-access-token.php Creates and evaluates expiring tokens.
tests/​test-access-token.php Tests token expiry behavior.
tests/​test-admin.php Tests admin TTL validation.
tests/​test-authentication.php Tests expired-token rejection.
tests/​test-client.php Tests TTL persistence.
tests/​test-token-endpoint.php Tests expiry response fields.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread inc/admin/namespace.php
Comment thread inc/tokens/class-access-token.php Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@roborourke
roborourke merged commit 113b763 into WP-API:main Sep 23, 2026
44 checks passed
roborourke added a commit to humanmade/OAuth2 that referenced this pull request Sep 23, 2026
Brings in per-client token TTL (WP-API#77), RFC 9728 protected resource metadata (WP-API#84), PHP 7.4+ support (WP-API#86) and the dynamic WP test matrix (WP-API#87).

Conflicts were between PKCE and token TTL, which both add a client meta field. Both fields are kept everywhere. In Client::update() the PKCE branch writes only the meta keys the caller supplies, while upstream always wrote token_ttl and cleared it when omitted. token_ttl now follows the same preserve-when-omitted rule as the other fields: omitting it keeps the stored value, and passing '' or null clears it. Upstream's tests clear the TTL by passing '' explicitly, so they are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joehoyle

Copy link
Copy Markdown
Member

Thanks @roborourke

@abhishek-kaushik

Copy link
Copy Markdown
Collaborator Author

thanks a lot @roborourke for working on this, apologies @joehoyle that I could not pick this up

@abhishek-kaushik
abhishek-kaushik deleted the token-expiry branch September 24, 2026 06:00
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.

4 participants