From 991a9931c376576f3c269cba3e4dae50e3c36be8 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Fri, 14 Aug 2026 15:21:52 +0530 Subject: [PATCH 1/2] saml deeplink auth --- app/sagas/deepLinking.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index fac9e3292d..6da1e0bdcb 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -143,6 +143,21 @@ const handleOAuth = function* handleOAuth({ params }) { } }; +let consumedSamlToken; + +const handleSaml = function* handleSaml({ params }) { + const { credentialToken } = params; + if (!credentialToken || credentialToken === consumedSamlToken) { + return; + } + consumedSamlToken = credentialToken; + try { + yield loginOAuthOrSso({ saml: true, credentialToken }); + } catch (e) { + log(e); + } +}; + const handleShareExtension = function* handleOpen({ params }) { const server = UserPreferences.getString(CURRENT_SERVER); const user = UserPreferences.getString(`${TOKEN_KEY}-${server}`); @@ -175,6 +190,10 @@ const handleOpen = function* handleOpen({ params }) { yield handleOAuth({ params }); return; } + if (params.type === 'saml') { + yield handleSaml({ params }); + return; + } // If there's no host on the deep link params and the app is opened, just call appInit() let { host } = params; From 845106ee26b377562d6a48f24027be209e7497bb Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 20 Aug 2026 21:24:41 +0530 Subject: [PATCH 2/2] add unit tests --- app/sagas/__tests__/deepLinking.test.ts | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/sagas/__tests__/deepLinking.test.ts b/app/sagas/__tests__/deepLinking.test.ts index e6ceccde95..1e0dd9c424 100644 --- a/app/sagas/__tests__/deepLinking.test.ts +++ b/app/sagas/__tests__/deepLinking.test.ts @@ -585,3 +585,61 @@ describe('deepLinking saga — handleOAuth dedup guard', () => { }); }); }); + +describe('deepLinking saga — handleSaml', () => { + beforeEach(() => { + jest.mocked(loginOAuthOrSso).mockReset(); + jest.mocked(loginOAuthOrSso).mockResolvedValue(undefined as any); + }); + + it('redeems the SAML credential token through the regular saml login', async () => { + const store = setupStore(); + + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST, credentialToken: 'saml-fresh-A' } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + expect(jest.mocked(loginOAuthOrSso)).toHaveBeenCalledTimes(1); + expect(jest.mocked(loginOAuthOrSso)).toHaveBeenCalledWith({ saml: true, credentialToken: 'saml-fresh-A' }); + }); + + it('does not call loginOAuthOrSso when the credentialToken is missing', async () => { + const store = setupStore(); + + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + expect(jest.mocked(loginOAuthOrSso)).not.toHaveBeenCalled(); + }); + + it('does not redeem the same SAML credentialToken twice', async () => { + const store = setupStore(); + + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST, credentialToken: 'saml-dup-B' } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + // The credential token is single use on the server, so a replayed deep link must be suppressed. + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST, credentialToken: 'saml-dup-B' } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + expect(jest.mocked(loginOAuthOrSso)).toHaveBeenCalledTimes(1); + }); + + it('redeems a different SAML credentialToken after a previous one was consumed', async () => { + const store = setupStore(); + + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST, credentialToken: 'saml-first-C' } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + store.dispatch(deepLinkingOpen({ type: 'saml', host: HOST, credentialToken: 'saml-second-C' } as any)); + await flushSagaMicrotasks(); + await flushSagaMicrotasks(); + + expect(jest.mocked(loginOAuthOrSso)).toHaveBeenCalledTimes(2); + expect(jest.mocked(loginOAuthOrSso)).toHaveBeenNthCalledWith(2, { saml: true, credentialToken: 'saml-second-C' }); + }); +});