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ửi | target_platform | Bạn nhận lại |
|---|---|---|
| JWT của Locate | locate | Chính JWT đó, không đổi. Nó không được kiểm tra. |
| Token Shukah | shukah | Chính token đó, không đổi. Nó không được kiểm tra. |
| JWT của Locate | shukah | Một token Shukah được lưu cho client đó. JWT được kiểm tra trước. |
| Token Shukah | locate | Mộ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
Request
Không cần và không đọc header Authorization: token cần chuyển đổi nằm trong JSON body.
| Field | Vị trí | Kiểu | Bắt buộc | Ràng buộc | Mô tả |
|---|---|---|---|---|---|
token | body | string | có | 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_platform | body | string | có | 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-Type | header | string | có | 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:
| Field | Kiểu | Nullable | Định dạng / ràng buộc | Mô tả |
|---|---|---|---|---|
status | integer | không | Luôn là 200. | — |
data.token | string | không | JWT 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
| HTTP | Body | Khi 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ới401(đọ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 trongasyncHandler(src/routes/client.js:22). Handler có try/catch riêng, và mọi exception bên trong nó đều thành401(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.bodylà{}, dẫn tới400ở 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ào | target_platform | Điều gì xảy ra | Nguồn |
|---|---|---|---|
| JWT (ba segment) | locate | Trả 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 |
| Opaque | shukah | Trả lại không đổi, không verify. | src/controllers/client/exchange-token/index.js:35-41 |
| JWT | shukah | verifyAccessToken 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 |
| Opaque | locate | Nó 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
clienttrê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-
locatecho 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àiatcó độ phân giải một giây. Lần insert thứ hai đụng indextoken_hashunique 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_bridgevớisource_system: 'merchant':client_id, hash SHA-256 của token làmtoken_hash,is_active: true,created_at, vàtoken_rawchỉ khiNODE_ENVkhông phảiproduction(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ớitoken_rawchỉ khiNODE_ENVkhông phảiproduction(src/sync/mappers/token-bridge-mapper.js:7-25), và từ inbound pushPOST {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-locatethêm một dòng. Không gì thu hồi một JWT Locate. - Push là
POST {SHUKAH_HOST}/maeda/client/add-tokenvới{access_token, client_id: <shukah_id>}và các headerx-from-application: LOCATE_CLIENTvàAuthorization: Bearer <SHUKAH_ADMIN_TOKEN>. Nó là best effort và được await inline, không có timeout. Nó không làm gì khishukah_idlà 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
401cho "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 commitc2d1c9f(2026-07-08) service quả thật mount nó ở đó, không có prefix. Commitd837492(2026-07-10) chuyển mọi route client xuống dướiPREFIX_API_V1/clients, và host auth giờ trả404cho/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-
shukahtrả 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(branchdevelopment, 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ớiexchange-token,api/v1/clients,authentication.locatevàauth_sourcekhông thấy gì;src/entities/user/model/api/services/auth.service.ts:85-112(token đã lưu bị xóa khi gặp401ở:101-103);src/entities/settings/model/api/dto/settings.dto.ts:112-126(type client-settings không có fieldauth_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)