Skip to content

feat(outlook): Each user signs in, so a chat agent works on each user's own mailbox (1.0.0) - #4163

Open
ForeignKeyCN wants to merge 3 commits into
langgenius:mainfrom
ForeignKeyCN:feat/outlook-per-user-signin
Open

ForeignKeyCN wants to merge 3 commits into
langgenius:mainfrom
ForeignKeyCN:feat/outlook-per-user-signin

Conversation

@ForeignKeyCN

@ForeignKeyCN ForeignKeyCN commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Today every Outlook authorization is shared: with OAuth, everyone who uses the tool acts on the mailbox of the one person who signed in from the console; with a Service Principal, on a fixed mailbox. That doesn't fit the most common agent use case: an assistant that many people chat with, where "What's on my calendar today?" should mean the calendar of whoever is asking.

Dify's tool credentials are workspace-level, so this PR does it inside the plugin, without changing the existing modes:

Each user signs in (new Mode on the API Key credential)

  • The credential holds only the app registration (tenant, client ID, secret, cloud) and the Sign-in Callback URL. No user token is stored in Dify's credential.
  • The first time a chat user needs Outlook, the tool returns a Microsoft sign-in link instead of data (authorization code flow with PKCE and a random state). The agent passes the link on; the user signs in in the browser and says they are done; the agent calls the tool again.
  • A new plugin endpoint (GET /outlook/callback) receives the browser's return. It only stores the one-time code under the state the tool issued; it never sees the client secret or the PKCE verifier, so it cannot redeem the code itself. Unknown or expired states are rejected.
  • The tool redeems the code and keeps the user's tokens in plugin storage, keyed by a hash of client_id | Dify user ID and Fernet-encrypted with a key derived (HKDF) from the client secret. Tokens never appear in tool output or logs, so prompt injection cannot leak them. The user ID comes from Dify's runtime, never from the LLM.
  • Access tokens are refreshed in the background; a refresh token Microsoft does not rotate is kept. The user is asked to sign in again only when the refresh token is rejected (revoked, expired after inactivity, password change), not on network errors. Rotating the client secret signs everyone out.
  • New Sign Out of Outlook tool, outlook_sign_out (forgets the calling user's sign-in, e.g. to switch accounts; in the other modes it explains that the credential is shared). It is not called sign_out: the new Agent exposes tools by their bare name and rejects duplicates, so an agent with Outlook and Teams (which gets the same mode) would fail with "Duplicate Dify Tool name". The first answer after signing in names the account used.
  • The Signed in page warns that a user could be sent someone else's sign-in link, and offers a link to cancel the sign-in before it is redeemed.
  • Validation checks the fields, rejects a localhost callback URL (Dify shows one when ENDPOINT_URL_TEMPLATE is left at its default), and for a specific tenant proves the client ID and secret with a client-credentials token that is then discarded.
  • The Mailbox setting is refused in this mode: the tools always act on the signed-in user's /me.

List Events / Search Events show online meetings

  • Asked "do I have any Teams meetings?", the agent called List Events and then Get Event on every event, because the list carried only subject, times and organizer. List Events now also selects location, bodyPreview, isOnlineMeeting and onlineMeeting; both tools add the location and the join link to each line of the summary, and to the output variables as is_online_meeting / online_meeting_url.
  • The join link is Graph's own (onlineMeeting.joinUrl), or else a Teams / Zoom / Google Meet / Webex link found in the invitation text: invites from other organizations and hand-pasted links are not flagged as online meetings by Graph.

Docs

  • README rewritten as a step-by-step guide with screenshots: choosing a mode, registering the Entra app (permissions per mode, admin consent, secret), OAuth, Service Principal, and building a per-user agent end to end, including how long users stay signed in, which Dify user IDs are stable (console, Service API user, public web app per browser), security notes, and troubleshooting for every error we hit.
  • readme/README_zh_Hans.md (which held English, Japanese and Chinese together) is now Chinese only; readme/README_ja_JP.md added. Old screenshots replaced.
  • PRIVACY.md describes what is stored per user and for how long.

Existing OAuth and Service Principal authorizations keep working unchanged; the version goes to 1.0.0 because of the new mode and endpoint.

Release Notes

  • Each user signs in: a new authorization mode for chat agents. Each person chatting with the agent signs in once with their own Microsoft account, and the tools work on their own mailbox and calendar. Needs the new Outlook sign-in endpoint (plugin details → Endpoints) registered as a Web redirect URI on the app, and delegated Graph permissions.
  • New Sign Out of Outlook tool for this mode, to switch accounts.
  • List Events and Search Events show each event's location and online-meeting join link (Teams, or a Zoom / Google Meet / Webex link in the invitation), so an agent can tell which events are online meetings without opening each one.
  • Users' tokens are stored encrypted in the plugin's storage and never shown to the agent.
  • README rewritten with a step-by-step setup guide and screenshots (English, Chinese, Japanese).
  • Existing OAuth and Service Principal authorizations are unchanged.

Change Type

  • Documentation / non-plugin change
  • Non-LLM plugin (tools, extensions, datasource, etc.)
  • LLM plugin

Screenshots / Videos

Agent on a self-hosted Dify, using the Each user signs in authorization:

First question: sign-in link After "done": answer from the user's own mailbox Sign out, then the link again
Authorization form Agent uses the per-user authorization

Testing

  • Unit tests: 125 passed (pytest), including the online-meeting link detection, the full sign-in, cached and refreshed tokens, per-user isolation and encryption at rest, secret rotation, revoked vs. temporarily failing refresh, a refresh response without a new refresh token, the callback rejecting unknown/expired/forged states, the cancel link, the account shown after sign-in, and the localhost check.
  • End to end on a self-hosted Dify (Community 1.16.1) with an Agent app and a Microsoft 365 tenant: sign-in link → Microsoft sign-in → callback page → mail and calendar answered from the user's own mailbox → sign out → link again. A second browser (a different anonymous web-app user) is asked to sign in separately, as expected.
  • Checked on the server: the plugin's storage holds only Fernet ciphertext (no JWT or token field names), and the plugin daemon, API and worker logs contain no tokens.
  • Existing OAuth authorizations were upgraded in place and kept working.

Checklist

  • I have read the contribution guidelines
  • Version bumped in manifest.yaml (0.7.1 → 1.0.0)

…'s own mailbox (1.0.0)

- New "Each user signs in" mode on the API Key credential: the first time a chat user
  needs Outlook, the tool returns a Microsoft sign-in link (authorization code + PKCE);
  the plugin's new sign-in endpoint receives the browser's return, and the tools then
  act on that user's /me with their own token.
- Per-user tokens are kept in plugin storage, keyed by a hash of the Dify user ID and
  Fernet-encrypted with a key derived from the client secret; never in tool output.
- Refresh in the background; keep the refresh token when Microsoft omits a new one;
  ask to sign in again only when the refresh token is rejected.
- Sign Out tool; the first answer after signing in names the account used.
- Callback page warns about forwarded links and offers to cancel the sign-in.
- Credential validation rejects a localhost Sign-in Callback URL.
- README rewritten (en, zh_Hans, ja_JP) with a step-by-step guide and screenshots.
The new Agent exposes tools by their bare name and rejects duplicates, so an agent
with Outlook and Teams (both had sign_out) failed with "Duplicate Dify Tool name".
…g links

The events list only carried subject and times, so an agent asked "do I have any
Teams meetings?" opened every event with Get Event. List Events now also selects
location, bodyPreview, isOnlineMeeting and onlineMeeting; both tools show the
location and the join link (Graph's, or a Teams/Zoom/Meet/Webex link pasted into
the invitation) in the summary and as is_online_meeting / online_meeting_url.

This branch has not been deployed

No deployments
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.

1 participant