Contents Management: Auth + Ownership + SSE

Goal: Route all geo writes through Auth Service, enforce content ownership via JWT email matching, and return SSE streams for real-time client feedback.

Context

Geo write operations (save/update/delete) currently go directly from Gateway to Geospatial Service. This is wrong because:

  • Geospatial Service is an internal data service and should not be externally exposed
  • No content ownership tracking — anyone with a valid JWT can modify/delete anyone’s content
  • No real-time feedback to clients during multi-step write operations

Target Architecture

Public reads:         Client → Gateway → Web Service → Geospatial Service
Authenticated writes: Client → Gateway (/auth/geo/**) → Auth Service (SSE) → Geospatial Service
Internal only:        /geo/** no longer exposed through gateway

SSE Event Model

All write endpoints return Flux<ServerSentEvent<GeoSseEvent>> (text/event-stream):

{"step": "auth",      "message": "Token validated, user: user@email.com", "terminal": false}
{"step": "ownership", "message": "Ownership verified",                    "terminal": false}
{"step": "forward",   "message": "Forwarding to geospatial-service...",   "terminal": false}
{"step": "accepted",  "message": "Data accepted: youtubeContentID=xyz",   "terminal": true}

Steps: authownership (update/delete only) → forwardaccepted|updated|deleted|error

Note: On error at any stage, a terminal error event is emitted and the stream completes.


Ownership Enforcement Flow

  1. Auth Service extracts email from JWT (jwtUtil.extractUsername(token))
  2. Save: sets ownerEmail on payload, forwards to Geospatial Service
  3. Update/Delete: Auth Service GETs the document from Geospatial Service, compares ownerEmail with JWT email
  4. Mismatch → SSE error event “You do not own this content” (403)
  5. Match → proceeds with write operation

Geospatial Service has no security logic — it just stores ownerEmail as a data field.


Implementation Phases

M1. ServiceClient — Add POST/PUT/DELETE raw methods

File: common-api/.../client/ServiceClient.java

Method Signature
postRawReactive (baseUrl, path, body, responseType) → Mono<T>
putRawReactive (baseUrl, path, body, responseType) → Mono<T>
deleteRawReactive (baseUrl, path) → Mono<Void> (uses .toBodilessEntity())
*NonReactive wrappers postRawNonReactive(), putRawNonReactive(), deleteRawNonReactive()

M2. Geospatial Service — ownerEmail field + bug fix

File Change
.../entity/AdventureTubeData.java Add private String ownerEmail; with @Indexed, getter/setter, update constructor
.../service/AdventureTubeDataService.java Fix deleteByYoutubeContentID bug (data.getYoutubeContentID()data.getId()). Preserve ownerEmail in update()
.../repository/AdventureTubeDataRepository.java Add findByOwnerEmail(String)

M3. Auth Service — SSE geo write proxy with ownership

File Action
.../model/sse/GeoSseEvent.java NEW — step, message, terminal fields
.../service/GeoWriteService.java NEW — save(), update(), deleteById(), deleteByYoutubeContentID() returning Flux<ServerSentEvent<GeoSseEvent>> with ownership verification
.../controller/GeoWriteController.java NEW — @RequestMapping("/auth/geo"), all endpoints produce text/event-stream
.../exceptions/OwnershipViolationException.java NEW — extends BaseServiceException
.../exceptions/code/AuthErrorCode.java Add: GEO_OWNERSHIP_VIOLATION (403), GEO_DATA_NOT_FOUND (404), GEO_SERVICE_ERROR (500)
config-service/.../auth-service.yml Add geospatial-service.url, circuit breaker config for GEOSPATIAL-SERVICE

GeoWriteService key pattern (Flux.concat for sequential SSE):

public Flux<ServerSentEvent<GeoSseEvent>> deleteById(String authorization, String id) {
    return Mono.fromCallable(() -> extractEmail(authorization))
        .flatMapMany(email -> {
            var authEvent = Flux.just(sseEvent("auth", "Token validated, user: " + email, false));
            var ownershipAndDelete = verifyOwnership(email, "/geo/data/" + id)
                .flatMapMany(doc -> Flux.concat(
                    Flux.just(sseEvent("ownership", "Ownership verified", false)),
                    serviceClient.deleteRawReactive(geoServiceUrl, "/geo/data/" + id)
                        .then(Mono.just(sseEvent("deleted", "Content deleted successfully", true)))
                        .onErrorResume(e -> Mono.just(sseEvent("error", "Delete failed: " + e.getMessage(), true)))
                        .flux()
                ))
                .onErrorResume(OwnershipViolationException.class, e ->
                    Flux.just(sseEvent("error", e.getMessage(), true)));
            return Flux.concat(authEvent, ownershipAndDelete);
        })
        .onErrorResume(e -> Flux.just(sseEvent("error", "Authentication failed: " + e.getMessage(), true)));
}

M4. Gateway — Remove /geo/** route

File: gateway-service/.../config/GatewayConfig.java

  • Remove: .route("geo-service", r -> r.path("/geo/**")...)
  • Keep: geo-docs swagger route
  • /auth/geo/** already covered by existing /auth/** route
  • No RouterValidator changes needed — not in openEndPoints, so JWT is required

M5. Postman + Notion

  • Update Postman URLs: /geo/save/auth/geo/save, /geo/data/{id}/auth/geo/data/{id}, etc.
  • Update this Notion document with final architecture

Implementation Order

M1 and M2 can be done in parallel. M3 depends on both. M4 after M3. M5 last.

M1 (ServiceClient) ──┐
                      ├──→ M3 (Auth Service SSE) → M4 (Gateway) → M5 (Postman/Notion)
M2 (Geospatial)   ──┘

Key Design Decisions

  1. Auth Service uses JsonNode (not AdventureTubeData) to avoid coupling to geospatial entity classes
  2. Ownership enforced in Auth Service, not Geospatial Service — keeps geo as a simple internal data store
  3. SSE via Flux.concat — each step emits before the next begins, giving real sequential progress
  4. terminal: true on the last event signals clients to close the SSE connection
  5. Legacy documents (no ownerEmail) treated as unowned — ownership check fails
  6. POST /geo/save via Kafka — SSE can only confirm “accepted by Kafka”, not “saved to MongoDB”

Verification

  • POST /auth/geo/save with JWT → SSE: auth → forward → accepted. Check MongoDB for ownerEmail
  • DELETE /auth/geo/data/youtube/{id} with same user JWT → SSE: auth → ownership → deleted
  • Same DELETE with different user JWT → SSE: auth → error (ownership violation)
  • GET /web/geo/data → still works (public read, no auth)
  • GET /geo/data directly → 404 (route removed from gateway)
  • Check Zipkin traces: gateway → auth-service → geospatial-service

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top