User detail
Background
GET /api/v1/users is the auth service's general "whose token is this?" route. It was written before
the migration, and no Shukah API maps to it. With x-auth-from: SHUKAH it checks a token and returns
the client's profile.
It matters for the migration because other backends use it. order-v2 and the point service call it to turn the token they received into a client. That token can be a Shukah token or a Locate JWT, so this route decides which tokens the order-v2 point routes accept.
Feature
Send the token in Authorization and say which backend verifies it in x-auth-from. The callers
send SHUKAH.
- order-v2: its profile guard, which protects many client routes such as orders and gift cards, calls
this route with
x-auth-from: SHUKAH. - The point service: its client middleware calls this route to find out who the caller is. It sends
x-auth-fromfrom its ownAUTH_VERIFY_APPLICATIONsetting (SHUKAHby default), reads the client id from the answer, and gives its own caller401when this route does not answer with a2xx. - Other callers: Not documented yet.
The app itself does not call this route: the app snapshot of 2026-02-26 has no call to it. The app
does not need to send x-auth-from to the order-v2 point routes, because order-v2 and the point
service set it themselves when they call this route.
Which tokens work depends on the auth_source client setting (shukah or locate), which locate-go
owns. If the service cannot read the setting, it uses locate.
| Token | auth_source is not locate | auth_source is locate |
|---|---|---|
| Shukah token | Works. The service asks Shukah to check it (GET /maeda/api/verify-token) and returns Shukah's answer unchanged. | Works when the token is one of the service's active stored tokens. Not captured. |
| Locate JWT | Works only if Shukah knows the JWT, because the service still asks Shukah. | Never works at the pinned code: it always gets 401. This is a defect. |
On dev, on 2026-10-01, a Shukah token and a Locate JWT both got 200 with the same body. On
2026-10-02 that changed. Dev's auth_source was shukah (read from the public client settings), so
this route passes the token on to dev Shukah. The order-v2 point routes, which call this route, gave a
Locate JWT 401 and a Shukah token 200: dev Shukah no longer accepted the JWT (an inference; this
route itself was not called with x-auth-from that day). With auth_source set to locate the JWT
would fail too, because of the defect in the table. So on dev today only a Shukah token works here,
and no setting alone makes the Locate JWT work.
GET /api/v1/users
Request
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes, except for MERCHANT | Bearer <token>. The second space-separated part is the token. Missing gives 401. | The token to check. On dev today only a Shukah token passes (2026-10-02). |
x-auth-from | header | string | yes | One of SHUKAH, LOCATE_V2, ADMIN_SHUKAH, ADMIN_LOCATE or MERCHANT. It is case-sensitive: with Authorization present, shukah gives 422. The callers use SHUKAH, and the rest of this page is about it. | Which backend verifies the token. |
access-token, client, uid | header | string | MERCHANT only | All three are required for MERCHANT. | A merchant session. |
Response
For x-auth-from: SHUKAH, 200 with the client's profile as a bare object. The body depends on how
the service checked the token:
| How the token was checked | Body |
|---|---|
Relay: auth_source is not locate. | Shukah's own verify-token body, unchanged. On dev it had the same 13 fields as Verify token: status is active, sex is 0, 1 or 2, and the flags are 0 or 1. |
Local, Shukah token: auth_source is locate. | A different shape: status is the raw ACTIVE, sex is MALE, FEMALE or UNKNOWN, the four flags are booleans, and odoo_id is left out when the client has none. Not captured. |
Local, Locate JWT: auth_source is locate. | No success: 401 every time at the pinned code. |
{
"client_id": 356259,
"odoo_id": "1342170",
"contact_phone": "+966500000000",
"status": "active",
"email_address": "user@example.com",
"first_name": "Test",
"last_name": "User",
"sex": 1,
"birth_date": "1990-01-01",
"receive_offers": 0,
"push_enabled": 0,
"is_food_truck_user": 1,
"is_cod_disabled": 0
}
Captured on dev, 2026-10-01 (client 356259), once with a Shukah token and once with a Locate JWT: both calls returned this body. Personal fields replaced. On 2026-10-02 a Locate JWT got 401 from the point routes that call this route, so this body is what a Shukah token gets today.
Errors
| HTTP | Body | When |
|---|---|---|
401 | {"status":401,"message":"Unauthorization"} | There is no Authorization header (except for MERCHANT). Shukah rejected the token, or the token is not among the stored tokens. Any exception while the token is checked. auth_source is locate and the token is a Locate JWT. |
422 | {"status":422,"message":"Your application is not support"} | Authorization is present, but x-auth-from is missing or is not one of the five values. |
404 | {"status":404,"message":"User not found"} | x-auth-from: LOCATE_V2 only, when the legacy user is missing. |
500 | {"status":500,"message":"Internal server errors"} | The handler throws, or the MERCHANT upstream fails with anything but 401. |
The 401 body was captured on dev on 2026-10-01 with no token. The 422
Your application is not support body was captured on dev on 2026-10-02, with a Locate JWT and no
x-auth-from. The other bodies are from the code.
Try it
export AUTH_BASE_URL=https://dev.authentication.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT is expected to get 401 here on dev today (inference); see Feature
curl -s "$AUTH_BASE_URL/api/v1/users" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "x-auth-from: SHUKAH"
Expected on dev (2026-10-02, inference: this route was not called with x-auth-from that day, the
point routes that call it were): 200 and the profile shown under Response for a Shukah token, and
401 for a Locate JWT, because dev passes it on to Shukah, which no longer accepts it. On 2026-10-01
both tokens got 200. Without x-auth-from you get the 422 shown under Errors.
Watch out
- A Locate JWT is expected to get
401here on dev today (inference from the point routes, 2026-10-02), and at the pinned code it always would withauth_sourceset tolocate, so do not promise Locate JWT support on this route. x-auth-fromis case-sensitive:SHUKAHworks,shukahgets422. One service doc writes it in lower case.- In relay mode each call also goes to Shukah and updates a legacy user record, and a Locate JWT works only if Shukah knows it.
Details (for backend)
Only dev was checked: host https://dev.authentication.locate.sa, prefix /api/v1. The UAT and
production hosts and prefix: Not documented yet.
Callers
- The point service:
clientAuthMiddlewareneedsAuthorization: Bearer <token>, then callsGET {AUTH_SERVICE_URL}/api/v1/userswith that header,x-auth-fromset toAUTH_VERIFY_APPLICATION(defaultSHUKAH) andAccept: application/json. A non-2xxanswer becomes401. The client id is the first non-null ofclient_id,user_idandidin the answer, and it must be a positive integer. The caller's ownx-auth-fromis never forwarded. - order-v2:
UserProfileGuardmakes the same call with theAuthorizationheader it received andx-auth-from: SHUKAH. It is applied with@UseGuards(UserProfileGuard)on many client controllers, for example orders and gift cards.
The five x-auth-from values
Order of checks: MERCHANT first, then a missing Authorization (401), then the value of
x-auth-from (422), then a switch. The comparison is case-sensitive
(src/middlewares/user.js:13-44, src/shared/constant/auth.js:1-7).
x-auth-from | What happens | Success body | Source |
|---|---|---|---|
SHUKAH | The token is checked as in the next table. | The profile. | src/middlewares/user.js:36-37,100-187 |
LOCATE_V2 (the switch's default case) | The token is looked up in the legacy users collection. A legacy DEACTIVATE user gives 401. The handler then returns the stored user document, or 404. | The legacy user document. | src/middlewares/user.js:42-43,201-228; src/controllers/users/detail/handler.js:26-36 |
ADMIN_SHUKAH | The token must equal the configured admin token, else 401. | An empty 200. | src/middlewares/user.js:38-39,189-199; src/controllers/users/detail/handler.js:15-17 |
ADMIN_LOCATE | GET {LOCATE_GO_BASE_URL}/api/admin/api/v1/profile with the Bearer token. Any failure gives 401. | The upstream profile. | src/middlewares/user.js:40-41,250-275 |
MERCHANT | Needs the headers access-token, client and uid. A missing one gives 422 {"status":422,"message":"Unauthorization"}. Then GET {MERCHANT_BASE_URL}/api/v1/users/profile. An upstream 401 gives 401, other failures give 500. | The upstream profile. | src/middlewares/user.js:54-98 |
Missing or any other value, with Authorization present | 422 {"status":422,"message":"Your application is not support"}. | — | src/middlewares/user.js:28-33; src/shared/constant/error-message.js:62 |
How SHUKAH checks a token
auth_source | Locate JWT (exactly three dot-separated parts) | Opaque token | Source |
|---|---|---|---|
locate | verifyClientToken calls ClientService.findByToken, which throws TypeError: Client.findByToken(...).cacheQuery is not a function. The model's static is async, so it returns a Promise, and a Promise has no cacheQuery. The catch turns it into 401, every time. Offline check with a valid JWT and the real middleware. | Finds an active token_bridge row by raw token, then the client. Not found gives 401. Found sets the profile from toShukahClientProfile (raw types). | src/middlewares/user.js:104-130,230-248; src/services/client.js:12-35; src/models/client.js:163-175 |
Any other value (normally shukah) | Relayed: GET {SHUKAH_HOST}/maeda/api/verify-token with Authorization: Bearer <token>. A 200 with a body becomes the profile, as Shukah sent it. Any failure gives 401. | The same relay. | src/middlewares/user.js:132-186 |
getAuthSource() resolves the value in this order: the AUTH_SOURCE environment variable, then the
cached auth_source of the settings:client key in Redis, then GET {LOCATE_API_URL}/settings/client,
then the default locate (src/services/setting.js:12-51,
docs/plans/2026-08-25-point-ledger-shared-write-contract.md:408-421). A Kafka message from locate-go
refreshes the cache (src/events/settingConsumer.js:14-33,
docs/plans/2026-07-28-settings-broker-consumer.md:43-68).
- Neither
locatebranch checks the account status.ClientService.findOnefilters only onclient_id, andClient.findByTokenfilters only onis_deleted: falseand the token's phone. So this route does not enforce active-only the way theclientmiddleware does (code reading,src/services/client.js:5-17,src/models/client.js:163-175). - In the relay branch every call writes to the legacy
userscollection: an upsert from Shukah's payload.statusactivebecomesACTIVE, anything elseDEACTIVATE, andsex0becomesMALE, anything elseFEMALE. Thatsexrule differs from the profile's own coding (1male,2female) (src/middlewares/user.js:145-172). - The relay accepts a Locate JWT only if Shukah knows it. That depends on the push of new Locate JWTs
to Shukah (see Exchange token), so a client whose push failed or
was skipped (no Shukah id) would get
401here whileauth_sourceisshukah(code reading). - Timing on dev: 630 ms for the Shukah token and 1061 ms for the Locate JWT, against 161 to 331 ms for calls that stay inside the service. That fits an upstream hop.
What the two captures do not explain
Both captured calls returned the Shukah-format body (lower-case status, integer sex, 0 and 1
flags) and were slow. At the pinned code only the relay branch can produce that body for a Locate
JWT. So either dev's auth_source was not locate on 2026-10-01, and Shukah accepted the Locate JWT
(that would fit the exchange-token call that minted it having pushed it to Shukah, if the push is on
for dev), or dev is not running 6757aa4. Neither the code nor the docs say which: Not documented
yet. This is an inference, not a fact.
To tell them apart, compare the process uptime from the unauthenticated /health route (no prefix)
with the 2026-10-01 11:57 +0700 merge of 6757aa4, and read auth_source from the client settings
(src/routes/health.js:8, src/controllers/health/handler.js:11-19).
Answered on 2026-10-02. The public client settings
(GET https://dev.go.locate.sa/api/v1/settings/client, the host in the app's SETTINGS_BASE_URL)
returned "auth_source":"shukah". An AUTH_SOURCE environment variable would override it and cannot
be read from outside, so this is very likely, not proven. The /health uptime of 90,815 s means the
service started about 2026-10-01 06:43 UTC, 1 h 46 min after the 6757aa4 merge (04:57 UTC), so dev
most likely runs 6757aa4 or later. So the first explanation is the probable one: on 2026-10-01 the relay
branch ran and dev Shukah accepted the Locate JWT. On 2026-10-02 the same relay refused it, which is why the
order-v2 point routes give a Locate JWT 401 Invalid or expired token
(locate-point-service: services/api/src/services/client-auth.service.ts:17,22-29). Why dev Shukah
stopped accepting the JWT: Not documented yet. The likely cause is that the Locate-to-Shukah token
push (src/config/envConfig.js:22-35) did not reach, or no longer lives on, the Laravel port of dev
Shukah (inference).
The locate + JWT defect is also in origin/production at 4f3bc96 (2026-09-21), which has the
same src/services/client.js and src/models/client.js as 6757aa4. Whether anything runs from
that ref, and whether a fix is planned before auth_source flips: Not documented yet.
Where the docs and the code disagree (code wins)
- A design doc writes the header value as
x-auth-from: shukah(docs/plans/2026-08-25-point-ledger-shared-write-contract.md:418). The code compares withSHUKAHexactly, and the lower-case value gets422. - A design doc says Locate JWTs "short-circuit to
verifyClientToken… and were never broken" (docs/plans/2026-07-27-shukah-opaque-token-auth-fix.md:51-52). That was true on 2026-07-27 and has been stale since commit3396fd1(2026-07-28): withauth_sourceset tolocate, the JWT branch always gives401. - 2026-10-02: this page said a Shukah token and a Locate JWT both got
200on dev (2026-10-01), and its Try it expected200for either token. On 2026-10-02 dev'sauth_sourcewasshukahand dev Shukah no longer accepted the Locate JWT, so only a Shukah token works; withauth_sourceset tolocateevery JWT gets401becausesrc/services/client.js:12-17calls.cacheQueryon the Promise returned by theasyncClient.findByToken(src/models/client.js:163).
Sources
locate-authentication-service@6757aa4:src/routes/user.js:5-12,src/middlewares/user.js:13-275,src/controllers/users/detail/handler.js:9-45,src/services/client.js:5-35,src/models/client.js:163-175,src/services/setting.js:12-51,src/events/settingConsumer.js:14-33,src/shared/constant/auth.js:1-7,src/shared/constant/error-message.js:59,62,77,src/routes/health.js:8,src/controllers/health/handler.js:11-19locate-authentication-service@6757aa4docs:docs/plans/2026-08-25-point-ledger-shared-write-contract.md:408-421,docs/plans/2026-07-28-settings-broker-consumer.md:43-68,docs/plans/2026-07-27-shukah-opaque-token-auth-fix.md:51-52locate-point-service@2c44ce3:services/api/src/middleware/client-auth.ts:7-18,services/api/src/services/client-auth.service.ts:15-37locate-order-v2-api@d9e4e5f6:src/auth/guards/user-profile-auth.guard.ts:58-72,src/modules/client/orders/orders.controller.ts:84,src/modules/client/gift-card-code/gift-card-code.controller.ts:10,src/modules/client/point/point.controller.ts:17-45(the point routes have no guard and forwardAuthorizationto the point service)- Mobile snapshot
mobile/locate-client@e960c3a(branchdevelopment, 2026-02-26; the app's own code, not Locate): a search of the whole tree forapi/v1/usersfinds nothing locate-authentication-service@6757aa4:src/config/envConfig.js:22-35- Dev captures, 2026-10-02 (parity audit):
user-detail--locate-jwt,points--locate-jwt,points--locate-st; the dev client settings and the auth service/healthuptime, both read on 2026-10-02 - Dev captures, 2026-10-01:
locate-users-shukah-token,locate-users-locate-token,probe-locate-users,shukah-verify-token