Skip to content

Commit a1b05b6

Browse files
committed
fix(auth): enforce web-session-only access on WebUser dependencies
WebUserRead/WebUserWrite/WebUserOrAnonymous now check is_web_session() to reject API tokens (PATs, OATs). Previously these depended on web_read/web_write reserved scopes for this gate, but with those scopes removed, any token could access web-only endpoints (OAuth consent, email change, PAT management, etc.). Also removes web_read/web_write remnants from OrgPolicyGuard defaults and AuthorizeMembersManage/AuthorizeOrgDelete scope requirements. Test fixture updated to provide mock UserSession for User subjects so is_web_session() returns True in tests.
1 parent 1d19077 commit a1b05b6

3 files changed

Lines changed: 44 additions & 11 deletions

File tree

server/polar/auth/dependencies.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
SubjectType,
2121
User,
2222
is_anonymous,
23+
is_web_session,
2324
)
2425

2526
oidc_scheme = OpenIdConnect(
@@ -216,16 +217,38 @@ async def __call__(
216217
)
217218

218219

219-
_WebUserOrAnonymous = Authenticator(
220+
_WebUserOrAnonymousAuth = Authenticator(
220221
allowed_subjects={Anonymous, User},
221222
required_scopes=None,
222223
)
224+
225+
226+
async def _web_user_or_anonymous(
227+
auth_subject: Annotated[
228+
AuthSubject[Anonymous | User], Depends(_WebUserOrAnonymousAuth)
229+
],
230+
) -> AuthSubject[Anonymous | User]:
231+
"""Allow anonymous or web-session users. Reject API tokens."""
232+
if not is_anonymous(auth_subject) and not is_web_session(auth_subject):
233+
raise Unauthorized()
234+
return auth_subject
235+
236+
223237
WebUserOrAnonymous = Annotated[
224-
AuthSubject[Anonymous | User], Depends(_WebUserOrAnonymous)
238+
AuthSubject[Anonymous | User], Depends(_web_user_or_anonymous)
225239
]
226240

227-
_WebUserRead = Authenticator(allowed_subjects={User}, required_scopes=None)
228-
WebUserRead = Annotated[AuthSubject[User], Depends(_WebUserRead)]
241+
_WebUserAuth = Authenticator(allowed_subjects={User}, required_scopes=None)
242+
243+
244+
async def _web_user(
245+
auth_subject: Annotated[AuthSubject[User], Depends(_WebUserAuth)],
246+
) -> AuthSubject[User]:
247+
"""Allow web-session users only. Reject API tokens."""
248+
if not is_web_session(auth_subject):
249+
raise Unauthorized()
250+
return auth_subject
251+
229252

230-
_WebUserWrite = Authenticator(allowed_subjects={User}, required_scopes=None)
231-
WebUserWrite = Annotated[AuthSubject[User], Depends(_WebUserWrite)]
253+
WebUserRead = Annotated[AuthSubject[User], Depends(_web_user)]
254+
WebUserWrite = Annotated[AuthSubject[User], Depends(_web_user)]

server/polar/authz/dependencies.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ def OrgPolicyGuard(
6767

6868
_allowed = allowed_subjects or {User, Organization}
6969
_scopes = required_scopes or {
70-
Scope.web_read,
71-
Scope.web_write,
7270
Scope.organizations_read,
7371
Scope.organizations_write,
7472
}
@@ -138,7 +136,7 @@ async def _always_allow(
138136
OrgPolicyGuard(
139137
members.can_manage,
140138
allowed_subjects={User},
141-
required_scopes={Scope.web_write, Scope.organizations_write},
139+
required_scopes={Scope.organizations_write},
142140
)
143141
),
144142
]
@@ -148,7 +146,7 @@ async def _always_allow(
148146
OrgPolicyGuard(
149147
org_policy.can_delete,
150148
allowed_subjects={User},
151-
required_scopes={Scope.web_write, Scope.organizations_write},
149+
required_scopes={Scope.organizations_write},
152150
)
153151
),
154152
]

server/tests/fixtures/auth.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,19 @@ def auth_subject(
9898
)
9999
subjects_map["member_billing_manager"] = member_billing_manager
100100

101-
return AuthSubject(subjects_map[subject_key], auth_subject_fixture.scopes, None)
101+
subject = subjects_map[subject_key]
102+
103+
# For User subjects, provide a mock UserSession so is_web_session() returns True.
104+
# This matches real web session behavior where all User sessions are UserSessions.
105+
session: Any = None
106+
if isinstance(subject, User):
107+
from unittest.mock import MagicMock
108+
109+
from polar.models import UserSession
110+
111+
session = MagicMock(spec=UserSession)
112+
113+
return AuthSubject(subject, auth_subject_fixture.scopes, session)
102114

103115

104116
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:

0 commit comments

Comments
 (0)