Skip to content

FlowAuth: add token renewal endpoint (#7275)#7279

Merged
jakejellinek merged 2 commits intomasterfrom
flowauth/token-renewal
Apr 29, 2026
Merged

FlowAuth: add token renewal endpoint (#7275)#7279
jakejellinek merged 2 commits intomasterfrom
flowauth/token-renewal

Conversation

@jakejellinek
Copy link
Copy Markdown
Contributor

@jakejellinek jakejellinek commented Apr 28, 2026

Closes #7275.

Summary

New POST /tokens/tokens/<token_id>/renew endpoint that mints a fresh JWT reusing the original token's name and roles. The original TokenHistory row is left intact — its JWT remains valid until its own exp claim passes, giving consumers a natural overlap window to switch over without downtime.

This is the second half of the renewal story: combined with #7274 (drop the absolute expiry cap), it eliminates the multi-step admin-bump dance currently required to rotate long-lived service tokens (e.g. flowbot's Airflow FLOWAPI_TOKEN).

Why same-name renewal matters operationally

External monitoring tooling can key off TokenHistory.name to create services like FlowAuth_Token_<name>. Reusing the name on renewal keeps those alert channels stable across rotations — no reconfiguration needed.

Behaviour

  • Roles are recovered from the tokens_with_roles association added in FlowAuth: surface assigned roles on the user's token list #7273. The renewal must reproduce the original role set verbatim — no role substitution.
  • Authorization: the caller must own the token and still hold every role it was minted with. If a role has been revoked since, renewal returns 401 with a message listing the missing role names. (No silent re-issue of permissions the user can no longer legitimately request.)
  • Old row is left alive: a new TokenHistory row is inserted; the old one is untouched. There is no revocation mechanism in flowauth (FlowAPI just verifies the JWT signature), so the overlap window is bounded only by the original token's exp.
  • Lifetime: optional lifetime_minutes in the body is honoured if ≤ min(server.next_expiry(), role.next_expiry()); otherwise rejected with 400. Omitted means "max permitted" — same default as the mint endpoint.
  • Pre-FlowAuth: surface assigned roles on the user's token list #7273 tokens: rows with no associated roles (created before that table existed) cannot be renewed; the endpoint returns 400 and asks the caller to mint a new token instead.

Frontend

TokenList.jsx passes a Renew callback through to each active token. Token.jsx shows a Renew button for active (non-expired) tokens; clicking it calls the new endpoint and triggers a list refresh. Errors surface inline as a small caption beneath the row. Expired tokens get no button (the backend would reject anyway).

Tests

  • test_token_renewal_creates_new_row_with_same_name_and_roles — happy path: list goes from 1 row to 2, both with the same name and the same role set; new JWT differs from old.
  • test_token_renewal_rejects_other_users_tokens — 401 when the calling user doesn't own the token.
  • test_token_renewal_rejects_when_role_revoked — 401 with the missing role name in the message when a role has been removed from the user since mint.

Branch base

This branch is stacked on flowauth/token-roles-visible (the PR for #7273) because the renewal endpoint reads roles from that PR's tokens_with_roles table. Once #7276 lands on master, I'll change this PR's base to master and rebase — the diff against master will then be just this PR's commit.

Test plan

  • Backend: pytest flowauth/backend/tests/test_token_generation.py — three new tests pass; existing don't regress.
  • Frontend: log in, mint a token, click "Renew" on the active row → list refreshes with two rows of the same name; the new row has a later expiry. Old token still works for FlowAPI calls until its own expiry.
  • Try renewing a token belonging to another user (manually) → 401.
  • Remove a role from a user, try to renew their token → 401 with the role name in the error.
  • Combined with FlowAuth: drop the silent absolute-expiry cap (#7274) #7277 (drop expiry cap): the full "no admin-bump needed" rotation flow works end-to-end.

Related

Summary by CodeRabbit

Release Notes

  • New Features

    • Token renewal support: A new "Renew" button is now available in the token list for active tokens, allowing users to generate a fresh JWT without invalidating the original token until its expiration. Token name and roles are preserved during renewal.
  • Documentation

    • Updated changelog with token renewal feature details.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Walkthrough

This PR introduces a token renewal endpoint (POST /tokens/<token_id>/renew) that issues fresh JWTs whilst preserving original TokenHistory records. It includes backend validation for ownership and role consistency, frontend API wrapper and "Renew" button UI, comprehensive tests, and CHANGELOG documentation.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Documents the new token renewal endpoint and "Renew" button feature in the Unreleased section.
Backend Token Renewal Endpoint
flowauth/backend/flowauth/token_management.py
Introduces renew_token() Flask route handler for POST /tokens/<token_id>/renew with validation logic: enforces ownership, rejects renewal if roles are missing or revoked, computes maximum permitted expiry from server and role caps, validates optional lifetime_minutes parameter, generates fresh JWT, and creates new TokenHistory entry whilst preserving the original.
Backend Token Renewal Tests
flowauth/backend/tests/test_token_generation.py
Adds three test cases: successful renewal creating new TokenHistory entry with fresh JWT, unauthorised renewal attempt by different user, and role-consistency enforcement (renewal fails if user's roles were revoked post-mint).
Frontend API Integration
flowauth/frontend/src/util/api.js
Exports new async function renewToken(token_id, lifetime_minutes) that constructs a POST request to the renewal endpoint with optional lifetime parameter and returns the response.
Frontend Token Components
flowauth/frontend/src/Token.jsx, flowauth/frontend/src/TokenList.jsx
Token component adds renewing/renewError state, handleRenew() async handler, and conditional "Renew" button (visible only for non-expired tokens). TokenList imports renewToken, implements handleRenew() that triggers renewal and reloads the token list, and passes token ID and renewal callback to Token components.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant TokenUI as Token Component
    participant TokenList as TokenList Component
    participant API as API Module
    participant Backend as Flask Backend
    participant DB as Database

    User->>TokenUI: Clicks "Renew" button
    TokenUI->>TokenUI: handleRenew() triggered
    TokenUI->>TokenList: onRenew(token_id)
    TokenList->>TokenList: handleRenew(token_id)
    TokenList->>API: renewToken(token_id, lifetime_minutes)
    API->>Backend: POST /tokens/<token_id>/renew
    Backend->>Backend: Validate ownership
    Backend->>DB: Query TokenHistory, token_roles
    DB-->>Backend: Return token and roles data
    Backend->>Backend: Validate role consistency
    Backend->>Backend: Compute max expiry
    Backend->>Backend: Generate fresh JWT
    Backend->>DB: Insert new TokenHistory entry
    DB-->>Backend: Confirm insert
    Backend-->>API: Return {token, history_id}
    API-->>TokenList: Success response
    TokenList->>TokenList: Reload token list
    TokenList-->>User: Display renewed tokens
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Tokens spin fresh with a single click,
Old JWTs linger whilst new ones stick,
No lengthy mints, no roles to replay,
Service tokens renew without dismay! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'FlowAuth: add token renewal endpoint (#7275)' clearly and concisely describes the primary change: introducing a new token renewal endpoint. It is specific, directly related to the main changeset, and follows good conventions.
Linked Issues check ✅ Passed All objectives from issue #7275 are implemented: POST endpoint accepts optional lifetime_minutes; recovers original role set via tokens_with_roles; mints fresh JWT with same name/roles; inserts new TokenHistory row preserving old; enforces ownership and role consistency (401 on revocation); UI includes Renew button with lifetime picker; tests cover success, unauthorised renewal, and revoked roles.
Out of Scope Changes check ✅ Passed All changes are within scope: backend endpoint implementation, frontend UI components (Token and TokenList), API utility function, comprehensive tests, and changelog entry. No unrelated modifications or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch flowauth/token-renewal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@cypress
Copy link
Copy Markdown

cypress Bot commented Apr 28, 2026

FlowAuth    Run #25981

Run Properties:  status check passed Passed #25981  •  git commit 7d339c3173: ci: retrigger after CI flakes (build_docs flowdb timeout, role_list.js cypress)
Project FlowAuth
Branch Review flowauth/token-renewal
Run status status check passed Passed #25981
Run duration 01m 04s
Commit git commit 7d339c3173: ci: retrigger after CI flakes (build_docs flowdb timeout, role_list.js cypress)
Committer Joachim Jellinek
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

@jakejellinek jakejellinek force-pushed the flowauth/token-roles-visible branch from 5c75566 to 746153b Compare April 29, 2026 09:59
Base automatically changed from flowauth/token-roles-visible to master April 29, 2026 10:33
POST /tokens/tokens/<id>/renew mints a fresh JWT reusing the original
token's name and roles. The original TokenHistory row is left intact
— its JWT remains valid until its own exp claim passes, giving
consumers a natural overlap window to switch over without downtime.

Reusing the name keeps monitoring stable: tooling that creates
services keyed off TokenHistory.name (e.g. CheckMK service
"FlowAuth_Token_<name>") doesn't need any reconfiguration when a
token is rotated.

Renewal requires the caller to still hold every role the original
token was minted with; if a role has been revoked since, renewal
fails with 401 listing the missing roles. Optional lifetime_minutes
in the body lets the user request a shorter token, validated against
the current server/role caps; omitted means "max permitted".

Frontend: a Renew button appears on each active token in TokenList;
clicking it calls the new endpoint and refreshes the list. Expired
tokens get no button (the backend would reject anyway).

Depends on the tokens_with_roles association table from #7273 to
recover the role set; this branch is stacked on
flowauth/token-roles-visible and its base will move to master once
that PR lands.
@jakejellinek jakejellinek force-pushed the flowauth/token-renewal branch from 55f1139 to 2498728 Compare April 29, 2026 10:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flowauth/frontend/src/TokenList.jsx (1)

138-148: ⚠️ Potential issue | 🟡 Minor

Missing key prop on expired token list as well.

Same issue applies to the expired tokens list.

🔧 Proposed fix
             {expiredTokens.map((object) => (
               <Token
+                key={object.id}
                 id={object.id}
                 name={object.name}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flowauth/frontend/src/TokenList.jsx` around lines 138 - 148, The
expiredTokens list rendering in TokenList.jsx is missing the React key prop on
each <Token> instance; update the expiredTokens.map callback to pass a stable
unique key (e.g., key={object.id}) to the Token component so React can track
list items properly—locate the expiredTokens.map(...) block that renders <Token
id={object.id} ... /> and add key using the id (or another unique field) to each
Token element.
🧹 Nitpick comments (3)
flowauth/backend/tests/test_token_generation.py (1)

172-198: Good test for ownership enforcement.

Correctly verifies that an admin cannot renew another user's token.

Note: The static analysis warning about unused uid and admin_id variables is valid but harmless in test fixtures. You could prefix them with underscores (_uid, _admin_id) to suppress the warnings if desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flowauth/backend/tests/test_token_generation.py` around lines 172 - 198, The
test test_token_renewal_rejects_other_users_tokens defines unused variables uid
and admin_id from fixtures which trigger static-analysis warnings; to fix,
rename those to _uid and _admin_id (or prefix them with underscores) in the test
function (and adjust any references like uid/admin_id if used elsewhere) so the
variables remain available from the fixture but are marked as intentionally
unused, leaving the test logic (auth.login, client.post, other_id retrieval, and
the renew assertion) unchanged.
flowauth/frontend/src/util/api.js (1)

406-416: Consider validating parseInt result to avoid sending NaN.

If lifetime_minutes is a non-numeric string (e.g., user types "abc"), parseInt returns NaN, which would be serialised as null in JSON and sent to the backend. While the backend validates this correctly, catching invalid input earlier provides a better user experience.

♻️ Optional: Add NaN check
 export async function renewToken(token_id, lifetime_minutes) {
   const body = {};
   if (lifetime_minutes != null && lifetime_minutes !== "") {
-    body.lifetime_minutes = parseInt(lifetime_minutes, 10);
+    const parsed = parseInt(lifetime_minutes, 10);
+    if (!isNaN(parsed)) {
+      body.lifetime_minutes = parsed;
+    }
   }
   var dat = {
     method: "POST",
     body: JSON.stringify(body),
   };
   return await getResponse("/tokens/tokens/" + token_id + "/renew", dat);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flowauth/frontend/src/util/api.js` around lines 406 - 416, The renewToken
function currently assigns body.lifetime_minutes = parseInt(lifetime_minutes,
10) without checking for NaN; update renewToken to validate the parsed value
(e.g., const mins = parseInt(lifetime_minutes, 10) and if Number.isNaN(mins) or
!Number.isFinite(mins) then either omit body.lifetime_minutes or return/reject
with a clear validation error) so you never stringify and send NaN to the
backend; ensure you still call getResponse("/tokens/tokens/" + token_id +
"/renew", dat) only with a valid numeric lifetime_minutes or without that field.
flowauth/frontend/src/Token.jsx (1)

146-148: Consider updating PropTypes for new props.

The component now uses id, onRenew, and roles props but these aren't declared in propTypes. While this is partially a pre-existing issue, updating PropTypes would improve documentation and catch prop errors during development.

♻️ Suggested PropTypes update
 Token.propTypes = {
   classes: PropTypes.object.isRequired,
+  id: PropTypes.number.isRequired,
+  name: PropTypes.string.isRequired,
+  expiry: PropTypes.string.isRequired,
+  token: PropTypes.string.isRequired,
+  roles: PropTypes.arrayOf(
+    PropTypes.shape({
+      id: PropTypes.number,
+      name: PropTypes.string,
+    })
+  ),
+  onRenew: PropTypes.func,
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flowauth/frontend/src/Token.jsx` around lines 146 - 148, Token.propTypes is
missing declarations for the new props used by the component; update
Token.propTypes to include id (PropTypes.oneOfType([PropTypes.string,
PropTypes.number])), onRenew (PropTypes.func), and roles
(PropTypes.arrayOf(PropTypes.string)) alongside the existing classes:
PropTypes.object.isRequired so IDEs and React PropTypes checks catch misuse—add
these keys to the Token.propTypes object and mark only classes as required
unless other props must be required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flowauth/frontend/src/TokenList.jsx`:
- Around line 107-118: The mapped Token components in TokenList.jsx are missing
the required React list key; update the JSX mapping over activeTokens to pass a
unique key prop (use object.id) to the Token component (the same id value
already passed as id) so React can reconcile properly when rendering the Token
components.

---

Outside diff comments:
In `@flowauth/frontend/src/TokenList.jsx`:
- Around line 138-148: The expiredTokens list rendering in TokenList.jsx is
missing the React key prop on each <Token> instance; update the
expiredTokens.map callback to pass a stable unique key (e.g., key={object.id})
to the Token component so React can track list items properly—locate the
expiredTokens.map(...) block that renders <Token id={object.id} ... /> and add
key using the id (or another unique field) to each Token element.

---

Nitpick comments:
In `@flowauth/backend/tests/test_token_generation.py`:
- Around line 172-198: The test test_token_renewal_rejects_other_users_tokens
defines unused variables uid and admin_id from fixtures which trigger
static-analysis warnings; to fix, rename those to _uid and _admin_id (or prefix
them with underscores) in the test function (and adjust any references like
uid/admin_id if used elsewhere) so the variables remain available from the
fixture but are marked as intentionally unused, leaving the test logic
(auth.login, client.post, other_id retrieval, and the renew assertion)
unchanged.

In `@flowauth/frontend/src/Token.jsx`:
- Around line 146-148: Token.propTypes is missing declarations for the new props
used by the component; update Token.propTypes to include id
(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), onRenew
(PropTypes.func), and roles (PropTypes.arrayOf(PropTypes.string)) alongside the
existing classes: PropTypes.object.isRequired so IDEs and React PropTypes checks
catch misuse—add these keys to the Token.propTypes object and mark only classes
as required unless other props must be required.

In `@flowauth/frontend/src/util/api.js`:
- Around line 406-416: The renewToken function currently assigns
body.lifetime_minutes = parseInt(lifetime_minutes, 10) without checking for NaN;
update renewToken to validate the parsed value (e.g., const mins =
parseInt(lifetime_minutes, 10) and if Number.isNaN(mins) or
!Number.isFinite(mins) then either omit body.lifetime_minutes or return/reject
with a clear validation error) so you never stringify and send NaN to the
backend; ensure you still call getResponse("/tokens/tokens/" + token_id +
"/renew", dat) only with a valid numeric lifetime_minutes or without that field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4195b87a-3c8e-4d67-883b-b986264aebb3

📥 Commits

Reviewing files that changed from the base of the PR and between 09bec6a and 7d339c3.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • flowauth/backend/flowauth/token_management.py
  • flowauth/backend/tests/test_token_generation.py
  • flowauth/frontend/src/Token.jsx
  • flowauth/frontend/src/TokenList.jsx
  • flowauth/frontend/src/util/api.js

Comment on lines 107 to 118
{activeTokens.map((object) => (
<Token
id={object.id}
name={object.name}
expiry={object.expires}
token={object.token}
roles={object.roles}
classes={classes}
editAction={editAction}
onRenew={this.handleRenew}
/>
))}
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.

⚠️ Potential issue | 🟡 Minor

Missing key prop on mapped Token components.

React lists require a unique key prop for efficient reconciliation. The id is now available and should be used as the key.

🔧 Proposed fix
           {activeTokens.map((object) => (
             <Token
+              key={object.id}
               id={object.id}
               name={object.name}
               expiry={object.expires}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{activeTokens.map((object) => (
<Token
id={object.id}
name={object.name}
expiry={object.expires}
token={object.token}
roles={object.roles}
classes={classes}
editAction={editAction}
onRenew={this.handleRenew}
/>
))}
{activeTokens.map((object) => (
<Token
key={object.id}
id={object.id}
name={object.name}
expiry={object.expires}
token={object.token}
roles={object.roles}
classes={classes}
editAction={editAction}
onRenew={this.handleRenew}
/>
))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flowauth/frontend/src/TokenList.jsx` around lines 107 - 118, The mapped Token
components in TokenList.jsx are missing the required React list key; update the
JSX mapping over activeTokens to pass a unique key prop (use object.id) to the
Token component (the same id value already passed as id) so React can reconcile
properly when rendering the Token components.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.09%. Comparing base (09bec6a) to head (7d339c3).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
flowauth/backend/flowauth/token_management.py 75.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7279      +/-   ##
==========================================
- Coverage   92.13%   92.09%   -0.05%     
==========================================
  Files         278      278              
  Lines       10794    10826      +32     
  Branches      697      697              
==========================================
+ Hits         9945     9970      +25     
- Misses        696      704       +8     
+ Partials      153      152       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jakejellinek jakejellinek merged commit 3ca5548 into master Apr 29, 2026
38 of 40 checks passed
@jakejellinek jakejellinek deleted the flowauth/token-renewal branch April 29, 2026 12:49
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.

FlowAuth: add a token renewal endpoint so service tokens can be rotated without minting from scratch

1 participant