Chuyển tới nội dung chính

Exchange token

Bối cảnh​

Shukah cấp một token opaque. Auth service cấp JWT của Locate và từ chối mọi thứ khác trên các route /api/v1/clients/* cần đăng nhập: token Shukah nhận 401. Exchange token là cầu nối giữa hai bên. Nó chỉ tồn tại trên auth service. Shukah không có endpoint tương đương, nên không API Shukah nào ánh xạ tới nó.

Trong quá trình migrate, app có thể đang giữ token của hệ thống này mà cần gọi hệ thống kia. Endpoint này biến token đang giữ thành token mà hệ thống đích chấp nhận.

Tính năng​

Gửi token bạn đang giữ và hệ thống bạn sắp gọi (target_platform: shukah hoặc locate). Bạn không cần biết mình giữ loại token nào: service quyết định dựa trên hình dạng của nó. Ba phần ngăn cách bằng dấu chấm là JWT của Locate, và mọi thứ khác là token Shukah.

Token bạn gửitarget_platformBạn nhận lại
JWT của LocatelocateChính JWT đó, không đổi. Nó không được kiểm tra.
Token ShukahshukahChính token đó, không đổi. Nó không được kiểm tra.
JWT của LocateshukahMột token Shukah được lưu cho client đó. JWT được kiểm tra trước.
Token ShukahlocateMột JWT Locate mới cho client sở hữu token Shukah.

Dòng cuối là dòng duy nhất ghi dữ liệu. Mỗi lời gọi tạo một JWT Locate mới, lưu nó, và đẩy nó sang Shukah khi client có Shukah id và outbound push được bật. Các token Locate mà nó lưu không bao giờ bị xóa.

Flow chart nháp của auth service (2026-07-08, xem Sources ở mục Chi tiết) nói app nên dùng nó thế nào. Khi khởi động, app đọc client setting auth_source. Nếu có token đã lưu, nó gọi exchange token với token đó và target_platform đặt bằng auth_source. Nếu không có, nó đăng nhập bằng OTP trên hệ thống tương ứng. Flow chart đó là bản nháp, và nó vẫn ghi path cũ /maeda/api/exchange-token.

App không làm vậy trong snapshot ngày 2026-02-26. Không có code nào trong mobile app gọi exchange token, và app không có base URL của auth service cũng như setting auth_source. Vì vậy một user giữ token Shukah mà gọi Verify token trên Locate sẽ nhận 401, và app sau đó xóa token đã lưu, khiến user bị đăng xuất. Bản build app hiện tại, mới hơn snapshot, có gọi endpoint này hay không: Chưa được tài liệu hóa.

POST /api/v1/clients/exchange-token​

Endpoint mới — không có endpoint tương đương trên Shukah.

Request​

Không cần và không đọc header Authorization: token cần chuyển đổi nằm trong JSON body.

FieldVị tríKiểuBắt buộcRàng buộcMô tả
tokenbodystringcóKhông được rỗng. Ba phần ngăn cách bằng dấu chấm được coi là JWT của Locate. Ở bước này không kiểm tra gì thêm về token. Một giá trị không rỗng mà không phải string sẽ lỗi với 401.Token mà app đang giữ.
target_platformbodystringcóChính xác "shukah" hoặc "locate", chữ thường. Bất cứ giá trị nào khác cho 400.Hệ thống mà app sắp gọi.
Content-Typeheaderstringcóapplication/json. Không có nó thì body không được đọc, và lời gọi cho 400.—

Response​

200 kèm token cho hệ thống đích:

FieldKiểuNullableĐịnh dạng / ràng buộcMô tả
statusintegerkhôngLuôn là 200.—
data.tokenstringkhôngJWT của Locate khi target_platform là locate. Token Shukah khi là shukah.Token cho hệ thống đích.
{
"status": 200,
"data": {
"token": "<locate-jwt>"
}
}

Capture trên dev, 2026-10-01 (client 356259; chỉ có shape): một token Shukah gửi với target_platform: "locate". Giá trị token là placeholder, vì token không bao giờ được lưu trong repo này.

{
"status": 200,
"data": {
"token": "<shukah-token>"
}
}

Capture trên dev, 2026-10-01 (client 356259; chỉ có shape): JWT Locate đó gửi lại với target_platform: "shukah". Một token Shukah gửi với target_platform: "shukah" trả về không đổi.

Một JWT Locate gửi với target_platform: "shukah" có thể trả về {"status":200,"data":{}}, khi token Shukah đã lưu không có bản raw. Những môi trường nào bị ảnh hưởng: Chưa được tài liệu hóa (xem Chi tiết).

Lỗi​

HTTPBodyKhi nào
400{"status":400,"message":"Invalid token or target_platform"}token thiếu hoặc rỗng, hoặc target_platform không phải shukah hay locate, hoặc request không có Content-Type: application/json.
400{"status":400,"message":"Internal server error"}Body không phải JSON hợp lệ. Ngoài production, message là text của parser, ví dụ Unexpected end of JSON input, và có thêm field stack.
401{"status":401,"message":"Unauthorization"}JWT Locate sang shukah: chữ ký không hợp lệ, token sai định dạng, hoặc client không có token Shukah active. Token Shukah sang locate: không có token đã lưu nào khớp, hoặc client mà nó thuộc về không tồn tại. Mọi lỗi không mong đợi bên trong lời gọi cũng cho 401 này, ví dụ lỗi database, token không phải string, hoặc token trùng.

Body 400 đầu tiên được capture trên dev vào 2026-10-01, với JSON body rỗng và lần nữa với target_platform đặt là nope. Body 401 là cái mà code trả ở đây; lần chạy capture không kích hoạt nó trên route này. 400 cho JSON sai định dạng đến từ một offline test của code service, không phải từ dev.

Thử ngay​

Lời gọi này ghi dữ liệu: mỗi lần nó biến một token Shukah thành JWT Locate, nó lưu một token mới và đẩy sang Shukah. Service cũng ghi request body, kể cả token, vào log của nó. Đừng gửi hai lần exchange cho cùng một client trong vòng một giây.

export AUTH_BASE_URL=https://dev.authentication.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # see "Getting a test token" on the module overview
curl -s -X POST "$AUTH_BASE_URL/api/v1/clients/exchange-token" -H "Content-Type: application/json" -d "{\"token\":\"$SHUKAH_TOKEN\",\"target_platform\":\"locate\"}"

Kỳ vọng: 200 và một JWT Locate trong data.token. Giữ nó làm $TOKEN cho các trang khác.

Lưu ý​

  • Các trường hợp trả không đổi không kiểm tra token: mọi chuỗi ba phần gửi với target_platform: "locate" đều trả 200. Để kiểm tra một token, hãy gọi Verify token.
  • Token Shukah gửi với target_platform: "locate" mỗi lần đều lưu một token mới và đẩy sang Shukah. Hai lời gọi cho cùng một client trong cùng một giây có thể khiến lời gọi thứ hai lỗi với 401 (đọc code, chưa tái hiện).
  • Ở chế độ production chỉ những token mà Shukah đẩy live mới có bản raw đã lưu, và exchange cần nó. Hành vi trên production: Chưa được tài liệu hóa.

Chi tiết (cho backend)​

Chỉ kiểm tra dev: host https://dev.authentication.locate.sa, prefix /api/v1. Host và prefix của UAT và production: Chưa được tài liệu hóa. Dev không chạy với NODE_ENV=production: body lỗi của nó có field stack (capture 2026-10-01 trên các lỗi address-book, profile và setting), và handler chỉ thêm stack ngoài production (src/middlewares/error-handler.js:12-35). Nên trên dev token_raw được ghi, vì nó chỉ được lưu ngoài production (xem "Collection token_bridge" bên dưới).

Route​

  • Route không có middleware: không có guard client, và handler không được bọc trong asyncHandler (src/routes/client.js:22). Handler có try/catch riêng, và mọi exception bên trong nó đều thành 401 (src/controllers/client/exchange-token/index.js:10-96).
  • Body được đọc bởi express.json() (src/index.js:42). Request không có Content-Type: application/json để req.body là {}, dẫn tới 400 ở trên (mặc định của thư viện, chưa capture). Giới hạn body mặc định 100 kb của thư viện được áp dụng.
  • Request body, kể cả token, được ghi vào server log (src/controllers/client/exchange-token/index.js:32).

Từng tổ hợp làm gì​

Token vàotarget_platformĐiều gì xảy raNguồn
JWT (ba segment)locateTrả lại không đổi, không verify. Một chuỗi ba phần giả mạo cũng trả 200.src/controllers/client/exchange-token/index.js:35-41
OpaqueshukahTrả lại không đổi, không verify.src/controllers/client/exchange-token/index.js:35-41
JWTshukahverifyAccessToken chạy, và throw cho 401. Sau đó nó tìm một dòng token_bridge của client với source_system trong ['shukah','locate'] và is_active: true, không sắp xếp: token Shukah active nào cũng được chấp nhận (quyết định 2026-09-09). Không tìm thấy cho 401. Nếu không, nó trả token raw của dòng đó. 'locate' trong danh sách không bao giờ khớp được, vì enum là shukah hoặc merchant.src/controllers/client/exchange-token/index.js:44-67; src/models/token-bridge.js:5-23; docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:200-224
OpaquelocateNó tìm dòng theo token raw, không có filter is_active. Không tìm thấy cho 401. Nó tải client mà không kiểm tra status hay is_deleted. Không tìm thấy cho 401. Nó mint một JWT {client_id, phone}, insert một dòng token_bridge (source_system: 'merchant'), đẩy JWT sang Shukah (best effort), và trả 200.src/controllers/client/exchange-token/index.js:70-89
  • Client không active vẫn nhận JWT ở dòng cuối, nhưng guard client trên các route khác sẽ từ chối nó (src/middlewares/client.js:27-33, src/shared/constant/user.js:14).
  • Nếu dòng khớp không có token raw, key bị bỏ và body là {"status":200,"data":{}} (src/controllers/client/exchange-token/index.js:66; đọc code).
  • Endpoint không thể validate một token: nó chỉ kiểm tra khi phải tra cứu một thứ gì đó.
  • Hai lời gọi opaque-sang-locate cho cùng một client trong cùng một giây mint ra JWT giống hệt nhau, vì token không có phần ngẫu nhiên và iat có độ phân giải một giây. Lần insert thứ hai đụng index token_hash unique và catch-all trả 401. Đọc code và một offline determinism check, chưa tái hiện: Chưa được tài liệu hóa là trên dev có xảy ra hay không.

Collection token_bridge và việc đẩy sang Shukah​

  • Một JWT Locate được lưu trong token_bridge với source_system: 'merchant': client_id, hash SHA-256 của token làm token_hash, is_active: true, created_at, và token_raw chỉ khi NODE_ENV không phải production (src/services/merchant-token-bridge.js:6-16, src/shared/helpers/jwt.js:12).
  • Token Shukah (source_system: 'shukah') đến từ ETL của bảng token Shukah, với token_raw chỉ khi NODE_ENV không phải production (src/sync/mappers/token-bridge-mapper.js:7-25), và từ inbound push POST {PREFIX_API_V1}/admin/clients/tokens, luôn lưu token raw (src/routes/admin.js:27,47, src/controllers/admin/create-token/handler.js:12-29, src/services/shukah-token-bridge.js:8-33).
  • Exchange đọc token_raw ở cả hai chiều (src/controllers/client/exchange-token/index.js:66,72). Auth service không bao giờ xóa hay vô hiệu hóa các dòng của token Locate: mỗi lời gọi opaque-sang-locate thêm một dòng. Không gì thu hồi một JWT Locate.
  • Push là POST {SHUKAH_HOST}/maeda/client/add-token với {access_token, client_id: <shukah_id>} và các header x-from-application: LOCATE_CLIENT và Authorization: Bearer <SHUKAH_ADMIN_TOKEN>. Nó là best effort và được await inline, không có timeout. Nó không làm gì khi shukah_id là null, sync bị tắt, hoặc host hay admin token chưa được set (src/services/shukah-sync.js:13-75, src/config/envConfig.js:19-33). Đây là cách Shukah biết các JWT Locate (docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28).

Khi tài liệu và code mâu thuẫn (code thắng)​

  • Tài liệu của service liệt kê một 401 cho "JWT expired" (docs/api/client-auth-and-address-book.md:404). JWT Locate không có thời hạn (src/shared/helpers/jwt.js:4-6), nên trường hợp đó không thể xảy ra.
  • Flow chart nháp và design doc 2026-07-06 viết route này là POST /maeda/api/exchange-token. Tại commit c2d1c9f (2026-07-08) service quả thật mount nó ở đó, không có prefix. Commit d837492 (2026-07-10) chuyển mọi route client xuống dưới PREFIX_API_V1/clients, và host auth giờ trả 404 cho /maeda/*.
  • Hai tài liệu nói token raw "never populated in prod" (docs/guides/shukah-migration-schema-reference.md:113, docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:106-108). Writer của inbound push lưu nó vô điều kiện (src/services/shukah-token-bridge.js:16). Chỉ ETL và writer JWT-Locate bị giới hạn ở non-production (src/sync/mappers/token-bridge-mapper.js:22, src/services/merchant-token-bridge.js:14).

Chưa được tài liệu hóa​

  • Ở chế độ production, những token Shukah đã lưu nào mang bản raw, và lời gọi JWT-sang-shukah trả gì khi nó thiếu. Theo code thì là {"status":200,"data":{}}.
  • Outbound push sang Shukah có được bật trên dev hay không (SHUKAH_SYNC_ENABLED, SHUKAH_HOST, SHUKAH_ADMIN_TOKEN).
  • Hai lần exchange cho một client trong cùng một giây có thật sự lỗi hay không.
  • Bản build app hiện tại (mới hơn snapshot 2026-02-26) có gọi endpoint này hay không.

Sources​

  • locate-authentication-service@6757aa4: src/routes/client.js:22, src/controllers/client/exchange-token/index.js:10-96, src/index.js:42, src/shared/helpers/jwt.js:4-12, src/services/merchant-token-bridge.js:6-16, src/services/shukah-token-bridge.js:8-33, src/sync/mappers/token-bridge-mapper.js:7-25, src/models/token-bridge.js:5-23, src/routes/admin.js:27,47, src/controllers/admin/create-token/handler.js:12-29, src/services/shukah-sync.js:13-75, src/config/envConfig.js:19-33, src/middlewares/client.js:27-33, src/shared/constant/user.js:14, src/middlewares/error-handler.js:12-35, src/shared/constant/error-message.js:59
  • Tài liệu của locate-authentication-service@6757aa4: docs/api/client-auth-and-address-book.md §9 (dòng 372-408; thay đổi lần cuối 2026-07-27), docs/draft/flow-chart-shukah2locate.md:15-36,54,69-112 (2026-07-08), docs/plans/2026-07-06-shukah-auth-api-local.md:8-11,26, docs/plans/2026-07-08-token-bridge-generic-and-image-urls.md:106-108,196-224 (2026-09-09), docs/plans/2026-07-16-shukah-token-outbound-push.md:10-28, docs/guides/shukah-migration-schema-reference.md:113
  • Snapshot mobile mobile/locate-client@e960c3a (branch development, 2026-02-26; code của chính app, không phải Locate): tìm toàn bộ cây thư mục với exchange-token, api/v1/clients, authentication.locate và auth_source không thấy gì; src/entities/user/model/api/services/auth.service.ts:85-112 (token đã lưu bị xóa khi gặp 401 ở :101-103); src/entities/settings/model/api/dto/settings.dto.ts:112-126 (type client-settings không có field auth_source)
  • Capture dev, 2026-10-01: probe-locate-exchange-token, probe-auth-host-maeda-prefix, và các lời gọi exchange-token được ghi trong live-capture summary (chỉ có shape, token không được lưu)