Skip to content

Commit 29229dd

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 6ed10cf commit 29229dd

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
@@ -66,8 +66,6 @@ def OrgPolicyGuard(
6666

6767
_allowed = allowed_subjects or {User, Organization}
6868
_scopes = required_scopes or {
69-
Scope.web_read,
70-
Scope.web_write,
7169
Scope.organizations_read,
7270
Scope.organizations_write,
7371
}
@@ -137,7 +135,7 @@ async def _always_allow(
137135
OrgPolicyGuard(
138136
members.can_manage,
139137
allowed_subjects={User},
140-
required_scopes={Scope.web_write, Scope.organizations_write},
138+
required_scopes={Scope.organizations_write},
141139
)
142140
),
143141
]
@@ -147,7 +145,7 @@ async def _always_allow(
147145
OrgPolicyGuard(
148146
org_policy.can_delete,
149147
allowed_subjects={User},
150-
required_scopes={Scope.web_write, Scope.organizations_write},
148+
required_scopes={Scope.organizations_write},
151149
)
152150
),
153151
]

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)