221 lines
6.7 KiB
Markdown
221 lines
6.7 KiB
Markdown
# Login
|
|
|
|
Authenticate a user with their Ooredoo mobile number and 4-digit M-Faisa mPIN.
|
|
|
|
The flow is two requests:
|
|
|
|
1. `fetchSubscriberByMDN` — confirms the number has a registered, fully-KYC'd M-Faisa wallet before asking for the mPIN.
|
|
2. `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](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`
|
|
|
|
```json
|
|
{ "mdnId": "<encryptMobile(msisdn), base64>" }
|
|
```
|
|
|
|
### curl Example
|
|
|
|
```bash
|
|
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](01-encryption.md#html-safe-gson--escape).
|
|
|
|
### Response
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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` (missing `b`) is the **server's** spelling, not ours. The same value also appears as `pocketDetails[0].roleId`.
|
|
|
|
### Response — wrong PIN
|
|
|
|
The server returns a **JSON array** (not object) on failure:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"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:
|
|
|
|
```kotlin
|
|
val trimmed = raw.trimStart()
|
|
if (trimmed.startsWith("[")) {
|
|
// wrong PIN path
|
|
} else {
|
|
// success path
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation notes
|
|
|
|
- **Plaintext is `"960" + msisdn`.** The country code is prepended *inside* `MfaisaCrypto.encryptMobile` rather than at the call site.
|
|
- **`Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")` is not enough on its own.** You also need the explicit `OAEPParameterSpec` — see [01-encryption.md](01-encryption.md#reference-implementation-kotlin).
|
|
- **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](README.md).
|
|
|
|
---
|
|
|
|
|
|
|
|
---
|
|
|
|
> **Next →** [Transaction History](03-history.md) | **← Back to** [README](README.md)
|