Problem
The Token Service issues service tokens with a scope claim reflecting whatever scopes were granted to an OAuth2 client at creation time (backend-services/token-service/internal/services/service_token.go:32,48), and Core's token validator correctly parses this claim back out into TokenClaims.Scopes (backend-services/core/internal/services/token_validator.go:57).
However, ServiceOAuthMiddleware (backend-services/core/internal/auth/middleware.go:44-51) only extracts claims.Subject into ServiceInfo.ClientID — it never reads or stores claims.Scopes. None of the handlers behind it check scope either; e.g. SendNotification (backend-services/core/internal/api/v1/handler/notification.go:141-190) only uses the client ID (to tag the microapp ID in the notification's data payload), never scope.
As a result, scopes are fully wired up at token-issuance time (and documented — see docs/admin/portal-guide.md's "Available Scopes" table listing notifications:send) but never actually enforced anywhere downstream. Any registered micro-app service client can call any service-authenticated route — including /notifications/send — regardless of what scopes it was actually granted. A client created with only a "read" scope could still send push notifications to arbitrary user emails today.
Proposed changes
- Store
claims.Scopes in the request context alongside ClientID in ServiceOAuthMiddleware (extend ServiceInfo to include Scopes).
- Add a scope-check mechanism (e.g. a
RequireScope("notifications:send") middleware, mirroring the existing rbac.RequireGroups pattern already used for user routes) and apply it to /notifications/send and any future service-authenticated routes that should be scope-gated.
- Reject requests with a clear 403 when the caller's token doesn't include the required scope, rather than silently allowing any valid service token through.
- Add a test confirming a service token without the
notifications:send scope is rejected by the endpoint.
Why
Scopes exist specifically to let each micro-app's service client be granted only the specific capabilities it needs (least privilege) — right now that boundary is documented and issued but not actually enforced, meaning a compromised or misconfigured client with any scope can perform actions (like sending push notifications to arbitrary users) it was never supposed to be authorized for.
Problem
The Token Service issues service tokens with a
scopeclaim reflecting whatever scopes were granted to an OAuth2 client at creation time (backend-services/token-service/internal/services/service_token.go:32,48), and Core's token validator correctly parses this claim back out intoTokenClaims.Scopes(backend-services/core/internal/services/token_validator.go:57).However,
ServiceOAuthMiddleware(backend-services/core/internal/auth/middleware.go:44-51) only extractsclaims.SubjectintoServiceInfo.ClientID— it never reads or storesclaims.Scopes. None of the handlers behind it check scope either; e.g.SendNotification(backend-services/core/internal/api/v1/handler/notification.go:141-190) only uses the client ID (to tag the microapp ID in the notification's data payload), never scope.As a result, scopes are fully wired up at token-issuance time (and documented — see
docs/admin/portal-guide.md's "Available Scopes" table listingnotifications:send) but never actually enforced anywhere downstream. Any registered micro-app service client can call any service-authenticated route — including/notifications/send— regardless of what scopes it was actually granted. A client created with only a"read"scope could still send push notifications to arbitrary user emails today.Proposed changes
claims.Scopesin the request context alongsideClientIDinServiceOAuthMiddleware(extendServiceInfoto includeScopes).RequireScope("notifications:send")middleware, mirroring the existingrbac.RequireGroupspattern already used for user routes) and apply it to/notifications/sendand any future service-authenticated routes that should be scope-gated.notifications:sendscope is rejected by the endpoint.Why
Scopes exist specifically to let each micro-app's service client be granted only the specific capabilities it needs (least privilege) — right now that boundary is documented and issued but not actually enforced, meaning a compromised or misconfigured client with any scope can perform actions (like sending push notifications to arbitrary users) it was never supposed to be authorized for.