Skip to content

[Backend]: Service-authenticated routes don't enforce OAuth2 client scopes #104

Description

@MohamadNazik

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

  1. Store claims.Scopes in the request context alongside ClientID in ServiceOAuthMiddleware (extend ServiceInfo to include Scopes).
  2. 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.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions