6.7 KiB
Login
Authenticate a user with their Ooredoo mobile number and 4-digit M-Faisa mPIN.
The flow is two requests:
fetchSubscriberByMDN— confirms the number has a registered, fully-KYC'd M-Faisa wallet before asking for the mPIN.doMobileLogin— submits the mPIN and (on success) returns the session + pocket details.
All RSA encryption used below is specified in detail in 01-encryption.md — the mobile-key cipher is RSA/ECB/OAEPWithSHA-256AndMGF1Padding with the plaintext "960" + msisdn; the mPin cipher is RSA/ECB/OAEPWithSHA-1AndMGF1Padding with the plaintext pin + <6-char salt>.
Step 1: fetchSubscriberByMDN
Confirms the number has a usable M-Faisa wallet before prompting for the mPIN.
Endpoint
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/fetchSubscriberByMDN
Request
Content-Type: application/json; charset=UTF-8
{ "mdnId": "<encryptMobile(msisdn), base64>" }
curl Example
MDN_ENC=$(python tmp/mfaisa_encrypt.py mobile <msisdn>)
curl --request POST \
--url https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/fetchSubscriberByMDN \
--compressed \
--header 'Content-Type: application/json; charset=UTF-8' \
--header 'Host: superapp.ooredoo.mv' \
--header 'Connection: Keep-Alive' \
--header 'Accept-Encoding: gzip' \
--data "{\"mdnId\":\"${MDN_ENC//=/\\u003d}\"}"
Note the
${MDN_ENC//=/=}substitution — the server requires the Gson html-safe=escape.
Response
{
"success": true,
"message": "Operation completed successfully.",
"kycStatus": "Full KYC",
"name": "<First Name>",
"firstName": "<First Name>",
"lastName": "<Last Name>",
"language": "English",
"activationPending": false,
"passwordCreated": true,
"subscriberRegistered": true,
"userIdCreated": false
}
Decision matrix
| Condition | Thijooree behaviour |
|---|---|
subscriberRegistered = false |
Show: "User not registered. Please use the Ooredoo SuperApp to register your M-Faisa wallet and complete KYC, then come back to Thijooree." |
kycStatus != "Full KYC" |
Show: "Your M-Faisa wallet needs Full KYC. Please complete KYC in the Ooredoo SuperApp, then come back to Thijooree." |
passwordCreated = false |
Show: "Set your M-Faisa mPIN in the Ooredoo SuperApp first, then try again." |
activationPending = true |
Show: "Your M-Faisa wallet activation is still pending. Complete it in the Ooredoo SuperApp first." |
| Otherwise | Proceed to doMobileLogin |
Step 2: doMobileLogin
Submits the encrypted mPIN; the response contains the user's wallet pockets (E-Money MVR, optionally IMT MVR and PayPal USD).
Endpoint
POST https://superapp.ooredoo.mv/api/mfaisaa-bff/mfino/v1.1/web/doMobileLogin
Request
Content-Type: application/x-www-form-urlencoded
| Field | Value |
|---|---|
channel |
C03 (constant) |
formData |
JSON object (see below), html-safe-escaped (= → =) |
formDataCs |
null (literal string) |
formData JSON object:
{
"deviceGeoInfo": {
"appType": "CustomerAndroid",
"appversion": "1.0",
"deviceId": "<Settings.Secure.ANDROID_ID>",
"deviceManufacturer": "<Build.MANUFACTURER>",
"imieNumber": "<Settings.Secure.ANDROID_ID>",
"ipaddress": "11.22.33.55",
"latitude": "0.0",
"longitude": "0.0",
"simId": "<Settings.Secure.ANDROID_ID>"
},
"mPin": "<encryptPin(mpin), hex>",
"mobileNumber": "<encryptMobile(msisdn), base64>",
"role": "RETAIL_SUBSCRIBER",
"tenantCode": "ooredoo",
"userName": "<encryptMobile(msisdn), base64>"
}
Both mobileNumber and userName encrypt the same plaintext, but encrypt independently (so their ciphertexts differ — OAEP padding randomises the output).
ipaddress is the constant "11.22.33.55" in the official app — not the device's real IP.
Response — success
{
"success": true,
"loginExchangeKey": "<opaque hex token>",
"mobileLoginSessionTimeout": "240",
"kycStatus": "Full KYC",
"suscriberId": "<12-digit subscriber id>",
"pocketDetails": [
{
"name": "<Subscriber Name>",
"eMailId": "<user@example.com>",
"mdnId": "<msisdn>",
"roleId": "<12-digit role id>",
"walletId": "<11-digit wallet id>",
"offerId": "<offer id>",
"pocketSummaryDetailsArrayDTO": [
{
"pocketId": "<pocket id>",
"pocketType": "INTERNAL",
"pocketValueType": "EMONEY",
"nickName": "E-Money",
"balanceAmount": { "amount": 0.0, "currencyCode": "MVR" },
"isDefaultPocket": true,
"isSecondaryPocket": false,
"statusType": "ACTIVE",
"displayName": "E-Money"
},
{ "pocketValueType": "PAYPAL_USD", "...": "..." }
]
}
]
}
The typo
suscriberId(missingb) is the server's spelling, not ours. The same value also appears aspocketDetails[0].roleId.
Response — wrong PIN
The server returns a JSON array (not object) on failure:
[
{
"success": false,
"message": "validation errors",
"error": [
{
"objectName": "Credentials Criteria",
"attributeName": "mPin",
"attributeValue": "MPIN_NOT_VALID",
"errorMessage": "Invalid mobile number/ Password. Please check and retry. If you have forgotten your PIN please go to FORGOT PIN to reset PIN."
}
]
}
]
On the second-to-last attempt, the errorMessage changes to:
Provided login details are not valid, One more wrong attempt will lock your account.
Thijooree detects the warning by substring ("one more" / "will lock", case-insensitive) and surfaces it as a stronger inline error.
Distinguishing success from failure
The official app — and Thijooree — distinguish the two purely by the JSON shape:
val trimmed = raw.trimStart()
if (trimmed.startsWith("[")) {
// wrong PIN path
} else {
// success path
}
Implementation notes
- Plaintext is
"960" + msisdn. The country code is prepended insideMfaisaCrypto.encryptMobilerather than at the call site. Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")is not enough on its own. You also need the explicitOAEPParameterSpec— see 01-encryption.md.- The mPIN salt must be exactly 6 alphanumeric chars. Other lengths/charsets work for OAEP locally but were not seen in the official app and aren't worth deviating from.
- No User-Agent header, as noted in the README.
Next → Transaction History | ← Back to README