Repository navigation
feat(email-oauth): one-click Email oauth mail sign-in for V2 sites - #506
Merged
prathameshkurunkar7 merged 9 commits intoOct 22, 2026
Merged
Conversation
This was referenced Oct 18, 2026
Contributor
[High impact] The changes since the last review appear safe to merge. Reviews (7) · Last reviewed commit: "fix(email-oauth): mark the provider-unav..." · Reviewed by Greptile |
RitvikSardana
force-pushed
the
feat/email-oauth-relay
branch
from
October 18, 2026 18:31
ec2f75e to
69d609d
Compare
RitvikSardana
force-pushed
the
feat/email-oauth-relay
branch
from
October 18, 2026 18:40
69d609d to
fbeb3a2
Compare
…ng skips permissions
This was referenced Oct 22, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
A site on Frappe Cloud V2 can connect a mailbox in one click. The site uses the mail provider app that Frappe Cloud owns, for example Microsoft or Google. Central keeps the secret of each provider app. Central does not give the secret to a site. The site gets only the tokens for its own mailbox.
microsoft,google,yahooandyandexif the record is not there. Each new record is disabled. These hooks do not change a record that is already there.https://.central.api.email_oauth.start: a site calls this method through its Pilot. Central makes sure that the return URL is the email callback of a site on that server. Central keeps a note for 10 minutes, withstateas the key. Then Central sends back the sign-in link of the provider (PKCE, S256) and the scopes.central.api.email_oauth.callback: the browser comes to this method from the provider. It is the Redirect URI of each provider app, and eachstateworks one time only. Central sends the code back to the site only if the Central user can see the server of that site. If not, Central sendsnot_team_memberand no code. Thus a site cannot get the code of a different team. If the browser is logged out, Central sends it to the dashboard login one time, and then back with the same code.central.api.email_oauth.relay_token: a site calls this method through its Pilot. Central uses the app secret to change a code or a refresh token into new tokens at the provider. If the provider refuses, Central sends back the OAuth error code of the provider, for exampleinvalid_grant. Thus the site knows when a token is dead.startandrelay_tokensend back the codeprovider_unavailable. The site then shows its usual mailbox form. Thus an admin can always add a mailbox, also when a provider is off.authRedirect.ts: after login, the login page can send the browser back to the email callback.How it works
flowchart TD A["Admin selects a provider<br/>in Helpdesk or Desk"] -- "admin's site login" --> S1 subgraph Site["Site (frappe)"] S1{"Can this user create<br/>an Email Account?"} --> S2["Make state + PKCE verifier,<br/>keep a 10 min note: user, verifier"] end S2 -- "pilot_auth_token" --> P1 subgraph Pilot["Site's Pilot"] P1{"Token valid for this site?<br/>Method on the allowlist?"} --> P2["Send to Central"] end P2 -- "Pilot credential" --> C0 subgraph CS["Central: start"] C0{"Provider enabled?"} -- no --> C4["provider_unavailable:<br/>the site shows its usual form"] C0 -- yes --> C1["Pilot credential gives team + server"] C1 --> C2{"return_url on a site<br/>of this server?"} C2 --> C3["Keep a 10 min note under state,<br/>send back the provider's sign-in link"] end C3 -- "link goes back to the browser" --> M1["<b>Mail provider</b><br/>admin signs in and gives consent,<br/>browser goes to Central with code + state"] M1 -- "browser's Central login" --> CB1 subgraph CC["Central: callback"] CB1["Find the note by state, delete it"] --> CB2{"Can this user see<br/>the site's server?"} CB2 -- no --> CB3["Back to the site with<br/>not_team_member, no code"] CB2 -- yes --> CB4["Back to the site with the code"] end CB4 -- "admin's site login" --> F1 subgraph SC["Site: callback"] F1{"Note for this state,<br/>made by this user?"} --> F2["Change the code into tokens: Pilot, then Central relay_token.<br/>Central adds the app secret and asks the provider"] F2 --> F3["Save the Connected App (Token URI frappe-cloud-relay:{provider}, no secret),<br/>the Token Cache and the Email Account"] end F3 --> OK(["Mailbox connected"])Every 10 minutes after that:
flowchart TD R0["Scheduler: email_account.pull"] --> R1{"Access token expired?"} R1 -- yes --> R2{"Token URI starts with<br/>frappe-cloud-relay:?"} R2 -- no --> R3["Refresh at its own Token URI, as before"] R2 -- yes --> R4["Pilot, then Central relay_token<br/>with grant_type=refresh_token"] R4 --> R5(["New tokens in the Token Cache,<br/>IMAP and SMTP sign in with them"])The site does not keep a provider secret or a Central secret. Its only key is
pilot_auth_token, and it sends this key only to its own Pilot.The flow in detail
sequenceDiagram autonumber actor Admin as Admin's browser participant Site as Site (frappe) participant Pilot as Site's Pilot participant Central participant MP as Mail provider Admin->>Site: Add a mailbox Note over Site: state + PKCE verifier,<br/>kept in the site's cache for 10 min Site->>Pilot: start(provider, state, code_challenge, return_url) Pilot->>Central: start (Pilot credential) Note over Central: provider must be enabled, else provider_unavailable<br/>return_url must be the email callback<br/>of a site on this server Central-->>Site: authorize_url, client_id, scopes Site-->>Admin: redirect to authorize_url Admin->>MP: sign in and give consent MP-->>Admin: redirect to Central's callback Admin->>Central: callback(state, code) Note over Central: each note works one time only<br/>logged out: dashboard login one time, then back<br/>can this user see the server? else not_team_member Central-->>Admin: redirect to the site's callback Admin->>Site: callback(state, code) Site->>Pilot: relay_token(authorization_code, code, code_verifier) Pilot->>Central: relay_token Central->>MP: token request with Frappe Cloud's client secret MP-->>Central: access, refresh and ID token Central-->>Site: tokens (through Pilot) Note over Site: Connected App with Token URI<br/>frappe-cloud-relay:{provider}, no secret,<br/>and the Email Account loop when the access token expires Site->>Pilot: relay_token(refresh_token) Pilot->>Central: relay_token Central->>MP: refresh with the client secret MP-->>Central: new tokens Central-->>Site: new tokens (through Pilot) endSetup
Do these steps for each provider:
https://<central host>/api/method/central.api.email_oauth.callbackas a Web Redirect URI of the app.microsoft.For a provider that has no record, add a record with its authorize URL, token URL and scopes. The site side has its own list of the services that can sign in in one click (
ONE_CLICK_PROVIDERSin frappe/frappe#44677).Testing
bench --site <site> run-tests --module central.tests.test_email_oauth: 16 tests pass.node --test dashboard/tests/auth-redirect.test.mjs: 2 tests pass. They check the return URL of the login page.Related
startandrelay_tokenthrough its PilotCloses #84