Skip to main content

Verify token

Background​

On Shukah the app called GET /maeda/api/verify-token to check its saved token and load the profile. The auth service returns the same profile with the same Shukah field names, so the switch is mostly a change of base URL and path prefix. Three other things need care: it accepts only a Locate JWT, its 401 body is different, and it does not send the app-version headers that the app reads from this call.

Feature​

Send the Locate JWT from exchange-token and get the client's profile back. The call only reads: it does not refresh the token or change anything. It answers 401 when the token is missing or invalid, or when the client's account is not active.

In the app snapshot of 2026-02-26, the app calls it every time it starts, to check its saved token and load the profile, and again from the Home screen. When the answer is 401, the app deletes its stored token.

GET /api/v1/clients/verify-token​

Replaces ShukahGET /maeda/api/verify-token✅ Replaced
Adapt2 breaking · 2 to watch
  • 🔴 Shukah token gets 401 "Unauthorization"; send a Locate JWT (from exchange-token; keep the Shukah token)
  • 🔴 On this 401 the app deletes its stored token (auth.service.ts:101), so exchange the token first
  • 🟡 Locate 401 body is {status,message}, not the Laravel body; the app reads message only
  • 🟡 Locate sends no app-version headers (new_version etc.); Shukah's are unknown (no response headers captured)

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <locate-jwt>. A Shukah token is refused with 401.The Locate JWT. Today the app gets it from exchange-token and keeps the Shukah token; Locate verify-otp also returns one, but login stays on Shukah for now.
languagequerystringnoAccepted and ignored.Kept so the old Shukah query string still works.
Any other query key or headerquery, header—noAccepted and ignored, for example x-from-application.—

Response​

200 with the profile as a bare object (no status or data wrapper):

FieldTypeNullableFormat / constraintsDescription
client_idintegernoThe same number as on Shukah for a migrated client (356259 for the dev test client). Clients registered in Locate get ids from 1,000,000,001 up.The client's id. It is also inside the JWT.
odoo_idstringyesA numeric string on the test client; null when the client has none.The client's Odoo (ERP) id.
contact_phonestringyesThe phone exactly as stored; the service does not reformat it. App-registered phones are stored as the app sent them and migrated phones keep Shukah's format (whitespace removed in both).The client's phone.
statusstringnoactive, pending, suspended, blocked or expired. This route returns only active, because every other status gets 401.The account status.
email_addressstringyesNo format check. Can be "" after a profile update with "".The client's email.
first_namestringyesTrimmed, at most 100 characters on write. Can be "" after a profile update with "".First name.
last_namestringyesTrimmed, at most 100 characters on write. Can be "" after a profile update with "".Last name.
sexintegerno0 unknown or not given, 1 male, 2 female.Gender.
birth_datestringyesYYYY-MM-DD: the UTC date of the value that was written.Birth date.
receive_offersintegerno0 or 1.Whether the client receives offers.
push_enabledintegerno0 or 1.Whether push notifications are on.
is_food_truck_userintegerno0 or 1.Food-truck user flag.
is_cod_disabledintegerno0 or 1.1 when cash on delivery is switched off for the client.
{
"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); personal fields replaced.

Errors​

HTTPBodyWhen
401{"status":401,"message":"Unauthorization"}There is no Authorization header, or it has no token part.
401Same body.The token is not a Locate JWT: a Shukah token, a malformed token or one with a wrong signature.
401Same body.The token is valid but its client does not exist, or the client's status is not active (pending, suspended, blocked or expired).
500{"status":500,"message":"Internal server error"}An unexpected failure inside the service. Not captured. Outside production the message is the raw error text and a stack field is added.

The 401 body was captured on dev on 2026-10-01 with no token, and again with a Shukah token.

Try it​

export AUTH_BASE_URL=https://dev.authentication.locate.sa
export TOKEN='<locate-jwt>' # see "Getting a test token" on the module overview
curl -s "$AUTH_BASE_URL/api/v1/clients/verify-token" -H "Authorization: Bearer $TOKEN"

Expected: 200 and the profile shown under Response. With a Shukah token in $TOKEN you get the 401 shown under Errors.

Differences from Shukah​

The 200 body is the same: on 2026-10-01 both services returned the same 13 keys, types and values for client 356259. The rows below cover the token, the URL, the error body and the app-version headers.

AspectShukahLocateImpact on the app
TokenShukah opaque token.Locate JWT only. A Shukah token gets 401 (captured on dev, 2026-10-01).Exchange the Shukah token for the JWT before switching, and keep the Shukah token for order-v2.
URLGET $SHUKAH_BASE_URL/maeda/api/verify-tokenGET $AUTH_BASE_URL/api/v1/clients/verify-token. The auth host does not serve /maeda/*: it answers 404.Change the base URL and the path.
401 body{"name":"Unauthorized","message":"Your request was made with invalid credentials.","code":0,"status":401,"type":"Illuminate\\Auth\\AuthenticationException"}{"status":401,"message":"Unauthorization"}Anything that matches on the error text, name or type must change. status is 401 in both.
App-version headersThe app reads new_version, old_version, has_new_version and is_required_update from the response headers of any URL that contains api/verify-token or client/points (its default list), and uses them for its update popup. Whether Shukah sends them: Not documented yet, because the captures hold request headers only.Not sent: no Locate service sets these headers. The Locate URL /api/v1/clients/verify-token does not contain api/verify-token, so the app would not look for them there.If Shukah sends the headers today, the update popup silently stops appearing after the switch.

Watch out​

  • Only a Locate JWT works here. A Shukah token gets 401 and the app deletes its stored token on a 401, so exchange it for the JWT first, and keep the Shukah token: order-v2 still needs it.
  • A Locate token never expires and is never revoked, so 401 means a bad token or an inactive account, never a timeout.
  • The 401 body is Locate's own {"status":401,"message":"Unauthorization"} (spelled that way), not Shukah's.

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. Dev does not run with NODE_ENV=production: its error bodies carry a stack field (captured on 2026-10-01 on the address-book, profile and setting errors), and the handler adds stack only outside production (src/middlewares/error-handler.js:12-35).

Route and guard​

GET /clients/verify-token runs the client middleware and then a one-line handler that returns the client the middleware loaded (src/routes/client.js:13, src/controllers/client/verify-token/index.js:7).

  • The guard reads only the second space-separated part of Authorization. The first word (Bearer, Token, anything) is not checked. A header with no second part fails with "jwt must be provided" and gives 401 (src/middlewares/client.js:15-22,38-41).
  • A malformed token, a wrong signature or a Shukah token makes jwt.verify throw, and the guard answers 401 (src/middlewares/client.js:22,38-41).
  • The guard loads the client by the client_id in the token. A missing client gives 401 (src/middlewares/client.js:27-33).
  • Not checked: the soft-delete flag, the token's phone against the stored phone, the token_bridge rows, the device and the IP (src/services/client.js:5-10: the query is { client_id } only).

Token rules​

A Locate JWT is signed with HS256 and carries {client_id, phone, iat}. The service signs and verifies without options, so no token has an exp: nothing expires and nothing is revoked (src/shared/helpers/jwt.js:4-10).

Active-status rule​

Only ACTIVE may authenticate. CLIENT_ACTIVE_STATUSES is [ACTIVE], so PENDING, SUSPENDED, BLOCKED, EXPIRED and a missing status all give 401 (src/shared/constant/user.js:4-14, src/middlewares/client.js:27-33).

The client lookup is cached for CLIENT_CACHE_TTL seconds, default 300 (src/services/client.js:5-10). Writes through Mongoose save() clear the cache. The bulk ETL write (bulkWrite) does not, so a status change that comes from the Shukah sync can take up to that long to apply (src/models/client.js:158-161).

Profile mapper​

formatClientProfileForResponse builds the body (src/controllers/client/setting/mapper.js:24-38).

  • status is the lower-case form of the stored status. A stored value outside the five becomes pending (src/controllers/client/setting/mapper.js:5-11,28). The sync maps Shukah's status text to the stored status in src/sync/lib/casts.js:24-33.
  • sex comes from the stored gender: MALE is 1, FEMALE is 2, anything else is 0 (src/controllers/client/setting/mapper.js:14-18). The four flags are 0 or 1 (src/controllers/client/setting/mapper.js:20).
  • contact_phone is the stored phone. A phone registered through reset-otp is stored as sent, with whitespace removed (src/controllers/client/reset-otp/handler.js:40,56, src/sync/lib/casts.js:40-41). Migrated phones go through the same whitespace-stripping normaliser (src/sync/mappers/client-mapper.js:99).
  • Write rules behind the field constraints:
    • first_name and last_name are written trimmed. A profile update accepts null or a string of at most 100 characters after trimming, and answers 400 otherwise. "" is stored as "". The model also caps both names at 100 characters, so a registration with a longer name fails (src/controllers/client/update-profile/validate.js:4-6,24-25, src/controllers/client/update-profile/index.js:13-15, src/controllers/client/update-profile/parse-payload.js:5,11-12, src/models/client.js:45-52).
    • email_address: a profile update accepts any string or null, with no format check and no length limit, and stores it trimmed, so "" stays "" (src/controllers/client/update-profile/validate.js:26, src/controllers/client/update-profile/parse-payload.js:13, src/models/client.js:57-60).
    • birth_date is kept as a YYYY-MM-DD string, the UTC date of the value written. A profile update answers 400 for a value that new Date() cannot read, and null clears it (src/sync/lib/casts.js:2-11, src/controllers/client/update-profile/validate.js:27-30, src/controllers/client/update-profile/parse-payload.js:14-16). The service doc gives the format as YYYY-MM-DD, and null when empty (docs/api/client-auth-and-address-book.md:454).
  • The same object is returned by verify-otp (data), PUT /api/v1/clients/setting and PUT /api/v1/clients/:client_id. GET /api/v1/users returns a different object in one mode (see User detail).
  • Ids: the sync copies a migrated client's Shukah id into client_id (src/sync/mappers/client-mapper.js:90). Clients registered in Locate get 1,000,000,000 + sequence (src/models/counter.js:7,23).

Where the docs and the code disagree (code wins)​

  • The service doc says status is "active" or "inactive" (docs/api/client-auth-and-address-book.md:177). The code returns the five values above, and the same doc lists them at line 453.

Sources​

  • locate-authentication-service@6757aa4: src/routes/client.js:13, src/middlewares/client.js:8-42, src/controllers/client/verify-token/index.js:3-7, src/controllers/client/setting/mapper.js:5-38, src/shared/helpers/jwt.js:4-10, src/shared/constant/user.js:4-14, src/services/client.js:5-10, src/models/client.js:158-161, src/models/counter.js:7,23, src/sync/lib/casts.js:2-11,24-33,40-41, src/sync/mappers/client-mapper.js:90,99, src/shared/constant/error-message.js:59, src/middlewares/error-handler.js:12-35, src/models/client.js:45-52,57-60, src/controllers/client/update-profile/validate.js:4-6,24-30, src/controllers/client/update-profile/index.js:13-15, src/controllers/client/update-profile/parse-payload.js:5,11-16, src/controllers/client/reset-otp/handler.js:40,56
  • locate-authentication-service@6757aa4 docs: docs/api/client-auth-and-address-book.md §3 (lines 150-183, 453, 454; last changed 2026-07-27), docs/plans/2026-07-14-shukah-verify-token-api-local.md §2 (2026-07-14)
  • Mobile snapshot mobile/locate-client@e960c3a (branch development, 2026-02-26; the app's own code, not Locate): platforms/mobile/products/locate/app/sideEffects/effects/initSecondaryResources.tsx:37-43, platforms/mobile/products/locate/screens/home/mainInner/Home/components/Screen/Home.tsx:53, src/entities/user/model/services/init/init.ts:21, src/entities/user/model/services/fetchUserInfo/fetchUserInfo.ts:50, src/entities/user/model/api/services/auth.service.ts:85-112 (the stored token is deleted on a 401 at :101-103), src/shared/api/interceptors/createResponseInterceptor.ts:18-29, src/shared/features/featuresConfigs.ts:41,90,107-110, src/features/AppVersionUpdate/ui/AppVersionUpdatePopup/AppVersionUpdatePopup.tsx:64
  • A search of locate-authentication-service@6757aa4, locate-point-service@2c44ce3 and locate-order-v2-api@d9e4e5f6 for new_version, old_version, has_new_version and is_required_update finds no match (2026-10-01).
  • Dev captures, 2026-10-01: locate-verify-token, shukah-verify-token, locate-verify-token-with-shukah-token, probe-locate-verify-token, probe-shukah-verify-token, probe-auth-host-maeda-prefix