Banking Android Application

Banking Android Application

These topics are critical for banking and payment applications because mistakes can cause:

* Unauthorized access

* Session hijacking

* Duplicate payments

* Incorrect transaction status

* Financial loss

* Poor customer trust

flowchart LR
    A["Authentication"] --> B["Token Management"]
    B --> C["Payment Submission"]
    C --> D["Idempotency"]
    D --> E["Transaction Lifecycle"]
    E --> F["Status Reconciliation"]

---

1. Token Management

1.1 What is token management?

Token management controls the authenticated user session after login.

It includes:

* Access token

* Refresh token

* Token expiry

* Refresh-token rotation

* Simultaneous 401 handling

* Logout

Basic token flow

flowchart TD
    A["User logs in"] --> B["Server verifies credentials"]
    B --> C["Access and refresh tokens issued"]
    C --> D["Access protected APIs"]
    D --> E{"Access token valid?"}
    E -- Yes --> D
    E -- No --> F["Refresh tokens"]
    F --> D

---

1.2 Access token

An access token is a short-lived credential used to call protected APIs.

Authorization: Bearer ACCESS_TOKEN

Important points

* Short expiry time

* Sent with normal API requests

* Validated by the server

* Should remain in memory when practical

* Must only be transmitted through HTTPS

* Must never appear in logs or URL parameters

---

1.3 Refresh token

A refresh token obtains a new access token when the current access token expires.

Important points

* Longer-lived than an access token

* Sent only to the authentication server

* Requires stronger storage protection

* Should support server-side revocation

* Should normally be rotated

* Must be cleared during logout

Access token vs refresh token

Access tokenRefresh token
Calls protected APIsObtains new tokens
Short-livedLonger-lived
Sent frequentlySent only during refresh
Usually kept in memoryMay need encrypted persistence
Limited attack windowMore valuable if stolen

---

1.4 Token expiry

The authentication server may return:

{
  "access_token": "access-a1",
  "refresh_token": "refresh-r1",
  "expires_in": 900
}

Here, the access token expires after 900 seconds.

val expiresAt =
    System.currentTimeMillis() + response.expiresIn * 1_000L

Use a small safety buffer:

fun isExpired(expiresAt: Long): Boolean {
    val safetyBuffer = 30_000L
    return System.currentTimeMillis() >=
        expiresAt - safetyBuffer
}

The server remains authoritative. It can reject a token before its calculated expiry because of:

* Logout

* Password change

* Token revocation

* Account restriction

* Suspicious activity

* Session-policy changes

---

1.5 Refresh-token rotation

Refresh-token rotation means that every successful refresh returns a new refresh token and invalidates the old one.

Login:
Access A1 + Refresh R1

Refresh using R1:
Access A2 + Refresh R2

R1 becomes invalid
sequenceDiagram
    participant A as Android App
    participant S as Auth Server

    A->>S: Refresh using R1
    S->>S: Invalidate R1
    S-->>A: Return A2 and R2
    A->>A: Replace both tokens atomically

Why is rotation important?

If an old refresh token is used again, it may indicate token theft. The server can revoke the session or complete token family.

Important implementation rule

Save the new access and refresh tokens together:

interface TokenStore {
    suspend fun getTokens(): AuthTokens?
    suspend fun replaceTokens(tokens: AuthTokens)
    suspend fun clear()
}

Do not save the access token first and refresh token later. A crash between those operations can create an inconsistent session.

---

1.6 Simultaneous 401 handling

Multiple parallel APIs may receive 401 Unauthorized at the same time:

Accounts API     → 401
Balance API      → 401
Transactions API → 401
Profile API      → 401
If every request refreshes independently, refresh-token rotation may fail.

Correct approach

flowchart TD
    A["Multiple requests receive 401"] --> B{"Refresh already running?"}
    B -- No --> C["Start one refresh request"]
    B -- Yes --> D["Wait for existing refresh"]
    C --> E{"Refresh successful?"}
    E -- Yes --> F["Save new token pair"]
    F --> G["Retry waiting requests once"]
    D --> G
    E -- No --> H["Clear session"]
    H --> I["Open login screen"]

Android implementation

Use:

* OkHttp Authenticator

* Kotlin Mutex

* Single refresh request

* One retry per failed request

* Maximum retry check

* Proper logout after permanent refresh failure

Conceptual logic:

private val refreshMutex = Mutex()

suspend fun refreshSafely(
    failedAccessToken: String
): String? {
    return refreshMutex.withLock {
        val currentTokens = tokenStore.getTokens()
            ?: return@withLock null

        // Another request already refreshed the token.
        if (currentTokens.accessToken != failedAccessToken) {
            return@withLock currentTokens.accessToken
        }

        val newTokens = authApi.refresh(
            currentTokens.refreshToken
        )

        tokenStore.replaceTokens(newTokens)
        newTokens.accessToken
    }
}

Avoid infinite loops

If a retried request again returns `401`, do not continuously refresh. End the session or return the failure.

---

1.7 Logout

Logout is more than opening the login screen.

flowchart TD
    A["User selects logout"] --> B["Revoke server session"]
    B --> C["Clear local tokens"]
    C --> D["Clear sensitive cache"]
    D --> E["Cancel authenticated work"]
    E --> F["Clear notifications"]
    F --> G["Clear navigation back stack"]
    G --> H["Open login screen"]

Logout checklist

* Revoke the refresh token or session where supported.

* Clear access and refresh tokens.

* Clear sensitive Room data according to business requirements.

* Cancel user-specific WorkManager jobs.

* Close authenticated WebSockets.

* Clear sensitive notifications.

* Remove protected screens from the back stack.

* Clear user-specific in-memory data.

OWASP MASVS requires secure authentication protocols, correct local authentication and extra protection for sensitive operations. OWASP MASVS Authentication

---

Token management interview questions

Why do we need a refresh token?

It allows the application to obtain a new short-lived access token without asking the user to log in repeatedly.

Where should tokens be stored?

Keep the access token in memory when practical. If persistence is required, encrypt tokens and protect the encryption key using Android Keystore.

How do you handle five simultaneous 401 responses?

Run only one refresh request using synchronization such as a Mutex. Other requests wait and then retry once using the new access token.

What happens if token refresh fails?

For permanent authentication failures, clear the session and ask the user to log in again. Temporary network failures should be distinguished from invalid or revoked refresh tokens.

---

2. Transaction Lifecycle

2.1 What is a transaction lifecycle?

The transaction lifecycle represents every state through which a payment may pass.

A payment is not always immediately successful or failed.

Transaction states

StateMeaningApplication behaviour
InitiatedPayment request was createdSave the local attempt
ProcessingBank is processing the paymentDisable duplicate submission
PendingFinal result is delayedCheck status later
SuccessfulServer confirms completionDisplay receipt
FailedServer confirms rejection or failureShow reason
ReversedPrevious financial effect was undoneDisplay reversal
UnknownApp cannot determine the resultReconcile with server

State flow

stateDiagram-v2
    [*] --> Initiated
    Initiated --> Processing
    Processing --> Successful
    Processing --> Failed
    Processing --> Pending
    Processing --> Unknown
    Pending --> Successful
    Pending --> Failed
    Pending --> Reversed
    Pending --> Unknown
    Successful --> Reversed
    Unknown --> Processing
    Unknown --> Pending
    Unknown --> Successful
    Unknown --> Failed

Exact state names and transitions depend on the payment API being integrated.

---

2.2 Initiated

The application created and submitted a payment attempt.

Store:

* Local transaction ID

* Idempotency key

* Amount and currency

* Payee reference

* Creation time

* Initial status

Initiated does not mean money was transferred.

---

2.3 Processing

The bank or payment system is processing the transaction.

Recommended message:

Your payment is being processed.
Please do not submit it again.

Disable another submission for the same payment attempt.

---

2.4 Pending

The server knows the transaction exists, but the final result is not available.

Possible reasons:

* Banking network delay

* Fraud review

* External payment-system delay

* Additional authorization

* Scheduled execution

Pending is not failure.

---

2.5 Successful

The authoritative server confirms that the payment completed.

The receipt can display:

* Payment reference

* Amount and currency

* Payee

* Completion date

* Confirmed status

> HTTP 200 does not automatically mean payment success. It means the HTTP request was handled. Always inspect the business status.

---

2.6 Failed

The server confirms that the payment was rejected or could not be completed.

Examples:

* Insufficient funds

* Invalid beneficiary

* Limit exceeded

* Authorization failure

* Expired consent

* Business validation failure

Do not automatically retry business failures.

---

2.7 Reversed

A previously accepted or successful transaction has been undone.

Payment successful
       ↓
Settlement or processing issue
       ↓
Funds returned
       ↓
Payment reversed

Do not display reversal simply as failure. Clearly tell the user that an earlier payment was reversed.

---

2.8 Unknown

The application cannot determine the final result.

Common causes:

* Request timeout

* Response lost

* App process killed

* Network disconnected

* Status service unavailable

Unknown ≠ Failed
Unknown ≠ Successful

The application should check the authoritative transaction status.

---

Kotlin transaction model

enum class TransactionStatus {
    INITIATED,
    PROCESSING,
    PENDING,
    SUCCESSFUL,
    FAILED,
    REVERSED,
    UNKNOWN
}
data class PaymentTransaction(
    val localTransactionId: String,
    val serverPaymentId: String?,
    val idempotencyKey: String,
    val amountMinorUnits: Long,
    val currency: String,
    val status: TransactionStatus
)

Use minor units or BigDecimal for money:

// £10.50 represented as 1050 pence
val amountMinorUnits = 1050L

Avoid Double for financial amounts because floating-point calculations can introduce precision problems.

---

Transaction lifecycle interview questions

Is HTTP 200 equal to payment success?

No. The response’s business status determines payment success.

What is the difference between pending and unknown?

Pending means the server knows the transaction and reports that completion is delayed. Unknown means the application cannot currently determine the authoritative result.

Can a successful transaction later become reversed?

Yes. Settlement or payment-network events may reverse the financial effect.

Where should transaction status be stored?

The local database can cache the last known status, but the banking server remains the source of truth.

---

3. Idempotency

3.1 What is idempotency?

Idempotency ensures that sending the same logical payment request multiple times creates only one financial transaction.

One request      → One payment
Repeated request → Still one payment

Duplicate-payment scenario

flowchart TD
    A["App submits payment"] --> B["Server executes payment"]
    B --> C["Response is lost"]
    C --> D["App sends request again"]
    D --> E{"Same idempotency key?"}
    E -- Yes --> F["Return existing payment result"]
    E -- No --> G["Duplicate-payment risk"]

---

3.2 Idempotency key

Generate a unique key for each logical payment attempt:

val idempotencyKey = UUID.randomUUID().toString()

Example:

POST /domestic-payments
Authorization: Bearer ACCESS_TOKEN
x-idempotency-key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

UK Open Banking payment examples use x-idempotency-key. The same key must not be reused with a modified request body. Follow the exact bank and API-version contract. UK Open Banking idempotency profile

Important rules

Same logical payment → Same key
New logical payment  → New key

* Generate the key before calling the API.

* Persist it before submitting the payment.

* Reuse it when resolving the same payment attempt.

* Never change amount, currency or payee for the same key.

* Let the server enforce idempotency.

* Keep the key independent from the access token.

* Retain it for the reconciliation period required by the backend.

Why disabling the Pay button is insufficient

The application can still generate duplicates because of:

* Network retries

* App restart

* Multiple devices

* Malicious clients

* Process death

* Parallel requests

UI protection improves user experience, but only the server can guarantee financial idempotency.

---

Retrofit example

interface PaymentApi {

    @POST("domestic-payments")
    suspend fun createPayment(
        @Header("x-idempotency-key")
        idempotencyKey: String,
        @Body request: PaymentRequest
    ): PaymentResponse
}

Persist before submission

suspend fun preparePayment(
    request: PaymentRequest
): PaymentTransaction {
    val transaction = PaymentTransaction(
        localTransactionId = UUID.randomUUID().toString(),
        serverPaymentId = null,
        idempotencyKey = UUID.randomUUID().toString(),
        amountMinorUnits = request.amountMinorUnits,
        currency = request.currency,
        status = TransactionStatus.INITIATED
    )

    paymentDao.insert(transaction.toEntity())
    return transaction
}
If the application crashes after submission, it can recover the transaction and its original idempotency key.

---

Idempotency interview questions

Should we generate a new key when a request times out?

No, not for the same logical payment. Preserve the original key and first check the payment status.

Can the same key be used for a different amount?

No. The request body must remain logically identical.

Who must enforce idempotency?

The server. The mobile application supplies and preserves the key, but client-side logic can be bypassed.

What should the server store?

Typically, the key, request fingerprint, transaction ID, processing status and original result for the configured idempotency period.

---

4. Payment Timeout Handling

4.1 What does a timeout mean?

A timeout means the app did not receive a response within the allowed time.

It does not prove that the server failed to execute the payment.

sequenceDiagram
    participant A as Android App
    participant S as Payment Server

    A->>S: Submit payment
    S->>S: Execute payment
    S--xA: Response lost or delayed
    Note over A: App receives timeout

The account may already have been debited.

---

Incorrect handling

Payment times out
       ↓
Create a new payment request
       ↓
Possible duplicate payment

Correct handling

flowchart TD
    A["Payment request times out"] --> B["Mark local state as Unknown"]
    B --> C{"Server payment ID available?"}
    C -- Yes --> D["Call payment-status API"]
    C -- No --> E["Reconcile using reference or idempotency key"]
    D --> F["Receive authoritative status"]
    E --> F
    F --> G["Update local database and UI"]

Open Banking payment resources provide status-retrieval operations, although the exact resource and status values depend on the payment type. Open Banking payment-status example

---

Timeout-handling steps

1. Preserve the transaction record.

2. Preserve the original idempotency key.

3. Set the local status to UNKNOWN.

4. Disable another payment for the same attempt.

5. Query the payment-status endpoint.

6. Retry status checks using controlled backoff.

7. Update the UI from the server response.

8. Escalate to reconciliation or support if status remains unknown.

User message

We could not confirm the payment result.

Please do not make the payment again while we check its status.
Reference: ABC123

Retry decision table

SituationCorrect action
Validation failureCorrect the request
Insufficient fundsDisplay business failure
Authentication failureRe-authenticate
Payment request timeoutCheck payment status
Status API timeoutRetry status check with backoff
Unknown resultContinue reconciliation
Confirmed successfulDisplay receipt
Confirmed failedAllow a new attempt if appropriate
Confirmed payment absentFollow backend resubmission policy

---

Timeout-handling interview questions

Why shouldn’t we immediately retry a timed-out payment?

The server may have already processed it and only the response was lost. A new request could create a duplicate payment.

What status should the app display after an uncertain timeout?

UNKNOWN or an equivalent “Checking status” state—not successful or failed.

What if the payment ID was not received?

The backend should support reconciliation using the client transaction reference, interaction ID or idempotency key.

Can WorkManager check an uncertain transaction later?

Yes, if eventual background reconciliation is required. It should check status, not blindly submit another payment.

---

5. Authentication

5.1 Authentication factors

Authentication verifies the user’s identity.

FactorMeaningExample
KnowledgeSomething the user knowsPassword, MPIN
PossessionSomething the user hasRegistered device
InherenceSomething the user isFingerprint, face

Using two different factor categories provides multi-factor authentication.

---

5.2 Password authentication

A password is generally used for initial login or account recovery.

Secure flow

flowchart TD
    A["User enters password"] --> B["Send through HTTPS"]
    B --> C["Server verifies password"]
    C --> D["Server issues tokens"]
    D --> E["App clears password"]

Rules

* Never store the password locally.

* Never log it.

* Send it only through HTTPS.

* Clear it after authentication.

* Apply retry limits on the server.

* Use a secure recovery mechanism.

* Do not include passwords in analytics or crash reports.

---

5.3 MPIN authentication

An MPIN is a short numerical secret.

Rules

* Do not store it in Room or DataStore.

* Do not store a reversible encrypted copy.

* Validate through a trusted system.

* Apply attempt limits and lockout.

* Clear it immediately after use.

* Never log it.

* Protect against screenshots and accessibility leakage according to the threat model.

A four-digit MPIN has only 10,000 combinations, so a locally extracted hash can be brute-forced quickly.

---

5.4 OTP authentication

An OTP is:

* Short-lived

* Single-use

* Bound to a session or operation

* Validated by the server

sequenceDiagram
    participant U as User
    participant A as Android App
    participant S as Server

    S-->>U: Deliver OTP
    U->>A: Enter OTP
    A->>S: Submit OTP securely
    S->>S: Check value, expiry and purpose
    S-->>A: Return result
    A->>A: Clear OTP

Rules

* Never persist OTP in Room, DataStore or SavedStateHandle.

* Limit verification attempts.

* Expire it quickly.

* Allow only one successful use.

* Bind payment OTP to the intended action.

* Clear it after submission.

* Do not log it.

---

5.5 Biometric authentication

Use Android BiometricPrompt.

The mobile app does not receive or store the user’s raw biometric information.

flowchart TD
    A["App requests authentication"] --> B["Android BiometricPrompt"]
    B --> C{"Authentication successful?"}
    C -- No --> D["Deny operation"]
    C -- Yes --> E["Unlock Keystore-protected key"]
    E --> F["Continue secured flow"]
    F --> G["Server authorizes operation"]

Biometric use cases

* Unlock local session

* Unlock a Keystore key

* Access sensitive data

* Confirm user presence

* Participate in payment step-up

> Biometric success confirms local user presence. The server must still authorize the payment.

---

5.6 Device binding

Device binding associates an account or session with a registered device.

Recommended flow

sequenceDiagram
    participant A as Android App
    participant K as Android Keystore
    participant S as Bank Server

    A->>K: Generate key pair
    K-->>A: Return public key
    A->>S: Register public key
    S->>A: Send challenge
    A->>K: Sign challenge
    K-->>A: Return signature
    A->>S: Send signed challenge
    S->>S: Verify registered public key

Important points

* Generate the private key inside Android Keystore.

* Send only the public key to the server.

* Keep the private key non-exportable where supported.

* Use server challenges to prove device-key possession.

* Support device deregistration and replacement.

* Treat device binding as one authentication signal.

Do not use only:

* Device model

* IMEI

* Android ID

* Plain random value in preferences

These values are not sufficient cryptographic proof of device possession.

---

5.7 Step-up authentication

Step-up authentication means asking for fresh or additional verification before a sensitive operation.

Example operations

* Adding a beneficiary

* Making a high-value payment

* Changing the registered phone number

* Registering a new device

* Changing a password

* Exporting sensitive information

* Performing a transaction from a risky environment

flowchart TD
    A["Authenticated user"] --> B["Requests sensitive operation"]
    B --> C["Evaluate transaction risk"]
    C --> D{"Step-up required?"}
    D -- No --> E["Server authorizes operation"]
    D -- Yes --> F["Biometric, OTP or fresh credential"]
    F --> G{"Verification successful?"}
    G -- Yes --> E
    G -- No --> H["Reject operation"]

Step-up authentication should be:

* Based on risk and business rules

* Bound to the sensitive action

* Short-lived

* Validated by the server

* Invalidated after completion or expiry

---

Authentication comparison

MethodMain purposeStored locally?Key limitation
PasswordInitial/recovery authenticationNoCan be stolen or reused
MPINQuick knowledge-factor authenticationNoSmall combination space
OTPSingle-use verificationNoVulnerable to interception/social engineering
BiometricLocal user-presence verificationNo raw biometricDevice-local verification
Device bindingProve possession of registered device keyPrivate key in KeystoreDoes not prove user identity alone
Step-upProtect high-risk operationsNo reusable secretMust be bound to the operation

---

6. Complete Secure Payment Flow

flowchart TD
    A["User logs in"] --> B["Server issues token pair"]
    B --> C["User prepares payment"]
    C --> D["Generate and persist idempotency key"]
    D --> E["Perform step-up authentication"]
    E --> F["Submit payment"]
    F --> G{"Response received?"}
    G -- Yes --> H["Store server status"]
    G -- Timeout --> I["Mark transaction Unknown"]
    I --> J["Check payment status"]
    J --> H
    H --> K{"Final state?"}
    K -- Successful --> L["Show receipt"]
    K -- Failed --> M["Show failure"]
    K -- Pending --> N["Continue status checks"]
    K -- Reversed --> O["Show reversal"]

---

7. Common Interview Mistakes

* Saying access and refresh tokens are the same

* Storing tokens in plain SharedPreferences

* Starting multiple token-refresh calls

* Retrying infinitely after 401

* Treating HTTP 200 as payment success

* Treating timeout as payment failure

* Generating a new idempotency key during the same attempt

* Preventing duplicates only by disabling the button

* Storing passwords, MPINs or OTPs

* Treating biometric success as server authorization

* Implementing device binding using only Android ID

* Missing step-up authentication for sensitive payments

* Keeping transaction state only in ViewModel

* Using Double for financial amounts

---

8. Quick Interview Answers

Explain secure payment submission in one answer.

> Before submitting a payment, I create and persist a unique idempotency key. For sensitive payments, I perform step-up authentication. The server executes the payment idempotently and returns a payment ID and business status. If the request times out, I mark the transaction as unknown and query the status endpoint using the existing reference instead of creating another payment.

Explain simultaneous 401 handling.

> I use an OkHttp Authenticator with synchronization such as a Mutex. Only the first failed request refreshes the token. Other requests wait and reuse the updated token. Each request is retried only once, and permanent refresh failure ends the session.

Explain the difference between pending and unknown.

> Pending is an authoritative server status indicating that the transaction exists but is awaiting completion. Unknown is a client-side uncertainty where the app could not determine the result, usually because of a timeout or lost response.

Explain device binding.

> The app generates a key pair inside Android Keystore and registers the public key with the server. Later, it signs a server challenge using the private key. Successful signature verification proves possession of the registered device key.

Explain step-up authentication.

> Step-up authentication asks for additional or fresh verification before a high-risk action, such as adding a beneficiary or making a high-value transfer. It can use biometric authentication, OTP or a fresh credential, but the server must authorize the operation.

---

Final Revision Table

TopicInterview keyword
Access tokenShort-lived API credential
Refresh tokenLonger-lived token renewal credential
ExpiryProactive check plus server authority
Token rotationOld refresh token invalidated
Simultaneous 401Single-flight refresh using Mutex
LogoutRevoke server session and clear local state
InitiatedPayment attempt created
ProcessingCurrently being processed
PendingKnown but awaiting final status
SuccessfulServer-confirmed completion
FailedServer-confirmed rejection
ReversedPrevious financial effect undone
UnknownFinal result cannot be determined
IdempotencySame payment, same key, one effect
TimeoutCheck status before resubmission
PasswordNever store
MPINNever persist; limit attempts
OTPShort-lived, single-use and action-bound
BiometricLocal user-presence verification
Device bindingKeystore key plus server challenge
Step-upExtra verification for sensitive operations