Skip to content

Rework oauth consent flows - #9015

Open
knolleary wants to merge 3 commits into
mainfrom
oauth-approve-put
Open

knolleary wants to merge 3 commits into
mainfrom
oauth-approve-put

Conversation

@knolleary

Copy link
Copy Markdown
Member

Fixes https://github.com/FlowFuse/security/issues/136

This improves the end-to-end user validation of the Oauth flows used by MCP and ff-tools-plugin.

  1. It replaces GET /account/complete with PUT /account/complete for MCP/tools-plugin flows. Editor oauth flow still uses GET routes as they have additional protection round client_secret handling that is unavailable to the other flow types.
  2. For MCP, it records who completes the /consent request, and verifies the user matches the call to /complete
  3. It only allows browser sessions to access the OAuth flows - not bearer tokens

I have tested locally that Editor access works as well as tools plugin. I have not verified the MCP routes - however test cases have been added across the board.

@andypalmi
andypalmi self-requested a review October 21, 2026 16:13
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.18919% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.50%. Comparing base (3ef2877) to head (4cf759d).

Files with missing lines Patch % Lines
forge/routes/auth/oauth.js 89.18% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9015      +/-   ##
==========================================
+ Coverage   79.46%   79.50%   +0.03%     
==========================================
  Files         475      475              
  Lines       26028    26054      +26     
  Branches     6938     6944       +6     
==========================================
+ Hits        20684    20714      +30     
+ Misses       5344     5340       -4     
Flag Coverage Δ
backend 79.50% <89.18%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 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.

@andypalmi

Copy link
Copy Markdown
Contributor

Tested locally: OAuth unit and tools-plugin system tests pass, and a real MCP client completes connect, consent and token, then calls /mcp fine. A few small suggestions:

Reject route

  1. Could PUT /account/reject/:code use verifyBrowserSession like the other approval routes? Right now anyone with the request ID can cancel it.
app.put('/account/reject/:code', {
    preHandler: verifyBrowserSession,
    schema: {
  1. The rejects a request via PUT test would then need cookies: { sid }, plus one test that it returns 401 without a session.

Token endpoint defaults

  1. With the approvedBy check, the fallbacks at oauth.js#L627-L629 can no longer be reached, since consent always saves teamIds and toolPermissions. Would it be worth dropping them and rejecting a missing toolPermissions too, so it fails closed instead of falling back to read+write on all teams?
if (requestObject.approvedBy !== requestObject.userId || !requestObject.expiresAt || !requestObject.toolPermissions) {
    return badRequest(reply, 'access_denied', 'Access Denied')
}
...
teamIds: requestObject.teamIds,
grantExpiresAt: requestObject.expiresAt,
toolPermissions: requestObject.toolPermissions

AccessRequest.vue errors

  1. In AccessRequest.vue, a failed PUT currently does nothing visible. Maybe match AccessRequestMCP.vue:
<div v-if="error" class="text-red-600 text-sm mb-4">{{ error }}</div>
data () {
    return { error: null }
},
methods: {
    async allowAccess () {
        this.error = null
        try {
            const response = await client.put(`/account/complete/${this.$router.currentRoute.value.params.id}`, {})
            window.location.href = response.data.redirect
        } catch (err) {
            this.error = err.response?.data?.description || 'Failed to process request. Please try again.'
        }
    },
    // denyAccess: same, with /account/reject
}

This branch was successfully deployed

1 active deployment
staging — 4cf759da Deployed Oct 21, 2026 by knolleary via Deploy application #12678
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.

2 participants