Skip to content

[WorkplaceAI][PerUserAuth] Add hooks for connector code grant OAuth flow#253606

Merged
dennis-tismenko merged 14 commits intoelastic:connectors-auth-code-grantfrom
dennis-tismenko:connect-disconnect-oauth-action
Feb 23, 2026
Merged

[WorkplaceAI][PerUserAuth] Add hooks for connector code grant OAuth flow#253606
dennis-tismenko merged 14 commits intoelastic:connectors-auth-code-grantfrom
dennis-tismenko:connect-disconnect-oauth-action

Conversation

@dennis-tismenko
Copy link
Copy Markdown
Contributor

@dennis-tismenko dennis-tismenko commented Feb 18, 2026

Summary

Replaces the old useOAuthAuthorize hook with a full set of OAuth lifecycle hooks, a refactored server-side callback route, and a new disconnect route for the connector authorization code grant flow. Changes include:

  • New useConnectorOAuthConnect hook supports two modes: Redirect and NewTab (opens provider in a new tab, listens for completion via BroadcastChannel).
  • New useOAuthRedirectResult hook detects OAuth completion from URL query params, broadcasts the result to the opener tab, and auto-closes the tab when applicable.
  • New useConnectorOAuthDisconnect hook and POST .../connector/{id}/_oauth_disconnect server route to remove stored OAuth tokens.
  • Refactored OAuth callback route with CSP-compliant callback page (external script via data-* attributes).
  • Updated edit connector flyout with Authorize/Disconnect actions and success/error toasts.
  • Shared constants, types, enums, and unit tests.

Demo

Screenshot.2026-02-20.at.17.10.06.mp4

Checklist

Check the PR satisfies following conditions.

Reviewers should verify this PR satisfies this list as well.

  • Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support
  • Documentation was added for features that require explanation or tutorials
  • Unit or functional tests were updated or added to match the most common scenarios
  • If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the docker list
  • This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The release_note:breaking label should be applied in these situations.
  • Flaky Test Runner was used on any tests changed
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.

@dennis-tismenko dennis-tismenko added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting labels Feb 18, 2026
@dennis-tismenko dennis-tismenko force-pushed the connect-disconnect-oauth-action branch 5 times, most recently from 3091f9d to 00aad84 Compare February 18, 2026 08:31
@dennis-tismenko dennis-tismenko force-pushed the connect-disconnect-oauth-action branch from 00aad84 to 277972b Compare February 18, 2026 08:33
@dennis-tismenko dennis-tismenko marked this pull request as ready for review February 18, 2026 08:38
@dennis-tismenko dennis-tismenko requested a review from a team as a code owner February 18, 2026 08:38
@elastic-vault-github-plugin-prod elastic-vault-github-plugin-prod bot requested a review from a team as a code owner February 18, 2026 08:54
@elastic-vault-github-plugin-prod elastic-vault-github-plugin-prod bot requested a review from a team as a code owner February 18, 2026 09:07
Copy link
Copy Markdown
Member

@florent-leborgne florent-leborgne left a comment

Choose a reason for hiding this comment

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

LGTM for docs

@dennis-tismenko dennis-tismenko changed the title Add connect, disconnect, and handle redirect hooks for connector code grant OAuth flow [WorkplaceAI][PerUserAuth] Add hooks for connector code grant OAuth flow Feb 18, 2026
@jcger
Copy link
Copy Markdown
Contributor

jcger commented Feb 19, 2026

Starting my review. But I already saw a mistake of mine, sorry. We need to move the disconnect/authorize actions to the connectors list and remove them from the flyout. It's important to validate that users with edit privilege can create/update and connect/disconnect to a connector and users with view edit cannot create/update a connector but can connect/disconnect.

Image

maybe with this icons? You'll have to sync with our designer @joana-cps

Image

In order to be able to show connect/disconnect when it should, instead of both at the same time, we need to update the GET Connectors endpoint. While that isn't implemented, please, show both

@dennis-tismenko dennis-tismenko force-pushed the connect-disconnect-oauth-action branch from 9e4cc97 to 1a6179c Compare February 19, 2026 21:24
@dennis-tismenko
Copy link
Copy Markdown
Contributor Author

Starting my review. But I already saw a mistake of mine, sorry. We need to move the disconnect/authorize actions to the connectors list and remove them from the flyout. It's important to validate that users with edit privilege can create/update and connect/disconnect to a connector and users with view edit cannot create/update a connector but can connect/disconnect.

In order to be able to show connect/disconnect when it should, instead of both at the same time, we need to update the GET Connectors endpoint. While that isn't implemented, please, show both

Moved the UI to the Connectors table in 1a6179c.

Screenshot 2026-02-19 at 15 09 38@2x

I'd like to defer design polish and finalization until after we have the connected status on the connectors page, but I can start a thread if needed.

// HTML page instead of redirecting.
const requestedReturnUrl = req.body?.returnUrl;
let kibanaReturnUrl: string;
let kibanaReturnUrl: string | undefined;
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.

I might be missing context, but I don’t see an active caller passing returnUrl today. Is this needed for EARS or a near-term flow? If not, should we keep it as optional future plumbing, or prefer to remove and add back when needed?

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.

I need to double check but I don't remember actually calling with a custom returnUrl for EARS. I think it was more if we have kick off the authorization flow from somewhere else, like the Sources page in WorkplaceAI, we can set our own "returnUrl" to go back to the Active Sources tab for example. But given the refactors to the oauth_callback code I think it might not be needed anymore. Happy to remove it in my PR or a follow-up PR if it is indeed dead code atm.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm personally a fan of giving the hook consumers (places in Kibana that want to kickoff the OAuth flow for a connector) the flexibility of specifying a custom returnUrl rather than being completely opinionated as to where we send the user, even if we aren't using it immediately. Also, if any consumer wants to kickoff the flow with OAuthRedirectMode.Redirect then they should absolutely be using it with the redirectUrl param.

But I also understand not wanting to overengineer for scenarios we may not need, so happy to remove it + OAuthRedirectMode.Redirect if you feel that'd be best.

isAwaitingCallback: boolean;
}

const DEFAULT_TIMEOUT_MS = 60 * 1000 * 10; // 10 minutes
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.

IIUC this would keep the "connect" action in loading state up to 10 minutes. I think we should let somehow users know that it can take this long, otherwise it will feel like it's hanging

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it'd keep it in this state assuming that the user never completes or cancels the OAuth flow from the consent screen (like if the user just closes the opened tab).

I added a cancelConnect function to the hook response so that hook consumers can give users the opportunity to reset isAwaitingCallback. I've also updated the Connectors table to show a cancel button instead of a loading spinner. Again, we can polish the final UX state afterwards but I think this is a better intermediate state to avoid the infinite (/10 minute long) spinner situation 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented in e11b54e

@jcger
Copy link
Copy Markdown
Contributor

jcger commented Feb 20, 2026

Nice job! Didn't approve yet because of #253606 (comment), all others can be addressed later

@jcger
Copy link
Copy Markdown
Contributor

jcger commented Feb 20, 2026

Starting my review. But I already saw a mistake of mine, sorry. We need to move the disconnect/authorize actions to the connectors list and remove them from the flyout. It's important to validate that users with edit privilege can create/update and connect/disconnect to a connector and users with view edit cannot create/update a connector but can connect/disconnect.
In order to be able to show connect/disconnect when it should, instead of both at the same time, we need to update the GET Connectors endpoint. While that isn't implemented, please, show both

Moved the UI to the Connectors table in 1a6179c.

I'd like to defer design polish and finalization until after we have the connected status on the connectors page, but I can start a thread if needed.

Perfect, I created a task for it #254142

Copy link
Copy Markdown
Contributor

@lorenabalan lorenabalan left a comment

Choose a reason for hiding this comment

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

Nice tidy up and demo! A couple of questions, but the rest lgtm.

}
);
);
} else {
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.

Nice, thank you for this! I found myself having to do sth similar in my own branch.

const stateObject = result.saved_objects[0];

// Check if createdBy matches
if (createdBy && stateObject.attributes.createdBy !== createdBy) {
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.

Why do we need this? It's highly unlikely we'd get the same state value for 2 different objects. If the createdBy was somehow modified in between encryption and decryption, then we fail on L164 and handle it in L174-178.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a super valid point, I debated for a while whether it was worth adding it despite the chances of a hash collision being insanely low, but ended up concluding "better safe than sorry". But you're right, probably overengineering, I'll undo this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 6ed56e6

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.

I don't see this as a collision prevention, but security, ensuring that the same user that starts the auth process is the same that finish it will avoid that in that 10 minute gap a user logs out, logs in as another and gets the authorization with the wrong user

Copy link
Copy Markdown
Contributor Author

@dennis-tismenko dennis-tismenko Feb 20, 2026

Choose a reason for hiding this comment

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

Yeah true, that's an angle I didn't consider, was only looking at it from a collision standpoint. In the case you describe, it has a super slim exploitation vector, it'd have to be the exact same person changing accounts on the same Kibana instance. So not a vulnerability per-se, but if the user needs to be switching between accounts/profiles on Kibana on a regular basis and accidentally completes the flow on a different account than the one they started, they might be confused.

I'm good either way tbh, I'll defer to your best judgement @jcger — let me know if you want me to bring this change back as a safety fallback. @lorenabalan happy to hear your thoughts as well if you feel strongly about leaving this change out.

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.

Fair point, I don't mind keeping it in it!

Pushing a bit more for my understanding here, wouldn't we want oauth_state SO also stored in the per-user namespace, rather than in the global one? Or does that have any implications on the task manager periodically removing expired states?

Copy link
Copy Markdown
Contributor

@jcger jcger Feb 23, 2026

Choose a reason for hiding this comment

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

we should revisit this, yes. It should be safer to store it by namespace, but it will make clean-up and callback handling a little trickier?

Edit: Adding issue link #254438

},
});
}
const { username, profile_uid } = currentUser;
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.

💡 TIL about user profiles.

Copy link
Copy Markdown
Contributor Author

@dennis-tismenko dennis-tismenko Feb 20, 2026

Choose a reason for hiding this comment

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

Me too for this PR! The 💪 behind per-user auth.

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.

Thanks for adding this 🙏

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.

💡 That's a neat trick, with latest.

@elasticmachine
Copy link
Copy Markdown
Contributor

elasticmachine commented Feb 20, 2026

⏳ Build in-progress, with failures

Failed CI Steps

Test Failures

  • [job] [logs] Jest Integration Tests #10 / SO type registrations does not remove types from registrations without updating excludeOnUpgradeQuery
  • [job] [logs] Jest Integration Tests #10 / SO type registrations does not remove types from registrations without updating excludeOnUpgradeQuery

History

@jcger
Copy link
Copy Markdown
Contributor

jcger commented Feb 23, 2026

[job] [logs] Jest Integration Tests #10 / SO type registrations does not remove types from registrations without updating excludeOnUpgradeQuery

can be addressed in the feature branch

@dennis-tismenko dennis-tismenko merged commit b5f4bf4 into elastic:connectors-auth-code-grant Feb 23, 2026
11 of 13 checks passed
jcger added a commit that referenced this pull request Mar 18, 2026
## Description

Currently, all Kibana connectors use a shared service account for
authentication. This approach lacks per user level access support, as it
does not distinguish between individual users and service account user
levels of permission. To support more secure, flexible, and user-aware
integrations, we need to introduce per-user authentication for
connectors in Kibana, alongside the existing service account method.

## 2-step release

As there are changes that require a 2-step release, this PR won't add
`oauth_authorization_code` auth type to any connector type. Therefore,
it won't be usable for now. The changes that require a 2-step release
are:
- we are adding `refreshTokenExpiresAt` to AAD for `connector_token` SO
- we are adding `refreshToken` as an encrypted attribute for
`connector_token` SO

## Config to run this locally
```
uiSettings:
  overrides:
    'workflows:ui:enabled': true
server.publicBaseUrl: 'http://localhost:5601'
```
Also, the auth type needs to be used in a connector. Reach out privately
to get the necessary info.

## Involved PRs:

- #246655
- #251873
- #251717
- #252566
- #252104
- #252307
- #252262
- #252501
- #253606
- #254589
- #254916
- Rename rate limit kbn setting 15d2c19
- Fix refresh token 34708e5

---------

Co-authored-by: Sean Story <sean.story@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Lorena Bălan <lorena.balan@elastic.co>
Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com>
Co-authored-by: Dennis Tismenko <dennis.tismenko@elastic.co>
qn895 pushed a commit to qn895/kibana that referenced this pull request Mar 18, 2026
## Description

Currently, all Kibana connectors use a shared service account for
authentication. This approach lacks per user level access support, as it
does not distinguish between individual users and service account user
levels of permission. To support more secure, flexible, and user-aware
integrations, we need to introduce per-user authentication for
connectors in Kibana, alongside the existing service account method.

## 2-step release

As there are changes that require a 2-step release, this PR won't add
`oauth_authorization_code` auth type to any connector type. Therefore,
it won't be usable for now. The changes that require a 2-step release
are:
- we are adding `refreshTokenExpiresAt` to AAD for `connector_token` SO
- we are adding `refreshToken` as an encrypted attribute for
`connector_token` SO

## Config to run this locally
```
uiSettings:
  overrides:
    'workflows:ui:enabled': true
server.publicBaseUrl: 'http://localhost:5601'
```
Also, the auth type needs to be used in a connector. Reach out privately
to get the necessary info.

## Involved PRs:

- elastic#246655
- elastic#251873
- elastic#251717
- elastic#252566
- elastic#252104
- elastic#252307
- elastic#252262
- elastic#252501
- elastic#253606
- elastic#254589
- elastic#254916
- Rename rate limit kbn setting 15d2c19
- Fix refresh token 34708e5

---------

Co-authored-by: Sean Story <sean.story@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Lorena Bălan <lorena.balan@elastic.co>
Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com>
Co-authored-by: Dennis Tismenko <dennis.tismenko@elastic.co>
jeramysoucy pushed a commit to jeramysoucy/kibana that referenced this pull request Mar 26, 2026
## Description

Currently, all Kibana connectors use a shared service account for
authentication. This approach lacks per user level access support, as it
does not distinguish between individual users and service account user
levels of permission. To support more secure, flexible, and user-aware
integrations, we need to introduce per-user authentication for
connectors in Kibana, alongside the existing service account method.

## 2-step release

As there are changes that require a 2-step release, this PR won't add
`oauth_authorization_code` auth type to any connector type. Therefore,
it won't be usable for now. The changes that require a 2-step release
are:
- we are adding `refreshTokenExpiresAt` to AAD for `connector_token` SO
- we are adding `refreshToken` as an encrypted attribute for
`connector_token` SO

## Config to run this locally
```
uiSettings:
  overrides:
    'workflows:ui:enabled': true
server.publicBaseUrl: 'http://localhost:5601'
```
Also, the auth type needs to be used in a connector. Reach out privately
to get the necessary info.

## Involved PRs:

- elastic#246655
- elastic#251873
- elastic#251717
- elastic#252566
- elastic#252104
- elastic#252307
- elastic#252262
- elastic#252501
- elastic#253606
- elastic#254589
- elastic#254916
- Rename rate limit kbn setting 15d2c19
- Fix refresh token 34708e5

---------

Co-authored-by: Sean Story <sean.story@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Lorena Bălan <lorena.balan@elastic.co>
Co-authored-by: Janki Salvi <117571355+js-jankisalvi@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com>
Co-authored-by: Dennis Tismenko <dennis.tismenko@elastic.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ResponseOps][PerUserAuth] Connector list UI - connect/disconnect action

7 participants