Skip to content

fix: let an SSO service refresh its session through its provider's frame - #17

Open
marcioviniciusspiridigliozzi-dot wants to merge 1 commit into
nicojan:mainfrom
marcioviniciusspiridigliozzi-dot:fix/sso-third-party-cookies
Open

fix: let an SSO service refresh its session through its provider's frame#17
marcioviniciusspiridigliozzi-dot wants to merge 1 commit into
nicojan:mainfrom
marcioviniciusspiridigliozzi-dot:fix/sso-third-party-cookies

Conversation

@marcioviniciusspiridigliozzi-dot

Copy link
Copy Markdown
Contributor

Microsoft Teams cannot be signed into from inside Chorus. Its own "you need to sign in again" banner leads nowhere: the button reaches no login gateway, restarting does not clear it, and the state comes back about a day after any successful sign-in.

The cause

WebKit's Intelligent Tracking Prevention treats a cookie read from an iframe on another registrable domain as third-party, and blocks it. Enterprise SSO is built on exactly that: the service embeds a hidden frame pointed at its identity provider and reads the session cookie from there.

With the cookie blocked the silent refresh can only fail, so the service falls back to telling the user to sign in again — which runs the same silent flow and fails the same way.

What the page reports while this happens:

AADSTS50058: a silent sign-in request was sent but no user is signed in

followed by Teams loading its own report-third-party-cookies.html.

The roughly-daily rhythm is ITP's own. It grants third-party cookie access for 24 hours after a first-party user interaction, so the service works right after a sign-in and stops once that window closes. That periodicity is what convinced me — I had two other theories first, and both were wrong.

Nothing inside the app can end the loop. ITP's record is on disk, so restarting does not clear it, and no button in the page reaches a real sign-in.

What this gives up, stated plainly

Turning ITP off per store costs little that this app's isolation is not already paying for. Every service gets its own WKWebsiteDataStore, so one service can never read another's cookies whatever ITP does. What ITP adds on top is blocking third-party cookies within a single service's own container, where the only participants are that service and the identity provider it chose.

What is genuinely given up: inside one service's container, an embedded ad network can keep a cookie across the sites it appears on there. The content blocker already drops most of those requests, and the blast radius stops at the one service.

Whether that trade belongs on by default or behind a per-service toggle is your call, not mine to assume. I have it on by default here because the failure it fixes is total — the service is unusable — but the code is written so either answer is a small change. Say the word and I will move it behind a setting.

Implementation notes

The setter is SPI, so it is probed before use and its effect is read back, with a warning logged if either step fails. If a macOS release drops it, SSO services break the way they do today and the log says why.

It also takes a scalar BOOL, so it has to be called through its implementation with that signature. perform(_:with:) passes an object pointer in the argument slot, and any non-nil pointer reads as a true BOOL — which turns ITP on rather than off, silently. That cost me a day, so the comment says so plainly rather than leaving the next person to find it.

Verification, and its limits

By hand: with this in place Teams signed in and stayed signed in across restarts and past the 24-hour mark, with no new login prompted. I also checked it does not cost persistence elsewhere — zero storage errors on the services I run.

I only have one Entra tenant to test against, so I would not call this proven. If anyone on another tenant can confirm, that would be worth having before you trust it broadly. I noticed #14's reporter is on Teams and responsive — they may be willing.

Note on CI

This branches from main, which currently has an intermittent StoreRepair test flake; I sent a fix for it in #15. A red run here is most likely that rather than this change.

Microsoft Teams cannot be signed into from inside Chorus. Its own
"you need to sign in again" banner leads nowhere: the button reaches no
login gateway, restarting does not clear it, and the state comes back
about a day after any successful sign-in.

The cause is WebKit's Intelligent Tracking Prevention. ITP treats a
cookie read from an iframe on another registrable domain as third-party
and blocks it. Enterprise SSO is built on exactly that: the service
embeds a hidden frame pointed at its identity provider and reads the
session cookie from there. With the cookie blocked the silent refresh
can only fail, and the service falls back to telling the user to sign
in again, which runs the same silent flow and fails the same way.

What the page reports while this happens:

  AADSTS50058: a silent sign-in request was sent but no user is
  signed in

followed by Teams loading its own report-third-party-cookies.html.

The roughly-daily rhythm is ITP's own: it grants third-party cookie
access for 24 hours after a first-party user interaction, so the
service works right after a sign-in and stops once that window closes.
Nothing inside the app can end the loop, because ITP's record is on
disk and no button in the page reaches a real sign-in.

Turning ITP off per store costs little that this app's isolation is not
already paying for. Every service gets its own WKWebsiteDataStore, so
one service can never read another's cookies whatever ITP does. What
ITP adds on top is blocking third-party cookies *within* a single
service's own container, where the only participants are that service
and the identity provider it chose.

What is genuinely given up is stated in the code comment rather than
glossed: inside one service's container, an embedded ad network can
keep a cookie across the sites it appears on there. The content blocker
already drops most of those requests and the blast radius stops at the
one service, but whether that trade belongs on by default or behind a
per-service toggle is the project's call, not mine to assume. The code
is written so either answer is a small change.

The setter is SPI, so it is probed before use and its effect is read
back, with a warning logged if either step fails. It also takes a
scalar BOOL, so it has to be called through its implementation with
that signature: perform(_:with:) passes an object pointer in the
argument slot and any non-nil pointer reads as a true BOOL, which turns
ITP on rather than off and leaves no trace. That cost me a day, so the
comment says it plainly.

Verified by hand: with this in place Teams signed in and stayed signed
in across restarts and past the 24-hour mark, with no new login
prompted. I have no second tenant to test against, so a confirmation
from someone else on an Entra tenant would be worth having before this
is trusted broadly.
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