Reverse-engineering the pin code authentication flow for mobile apps
Tags: security, mobile, architecture, api
Published: 2021-12-26 11:23:22

## Investigating The Existing Applications

I am developing a mobile application for financial usage. I want to make it as secure as the existing apps on the market. Many apps ask the user to enter a PIN code to unlock the app after period of time or when the user wants to perform some important actions such as transferring the money.

But I cannot find a standard (similar to OAuth) or reliable document about its implementation or the reason behind the flow design. Every app seems to have some variations. For example

- V1) Some apps bind the pin code with a device, if the user uses another device, they have to set up a new pin code. While the other apps allow the user to use the same pin code for many devices at the same time.


- V2) Some apps store and validate the pin code at the server-side, while the others use the pin code to encrypt a kind of token, which can be used to exchange for an access token, and store the encrypted value at the device.


- V3) Some apps force the user to set up a new pin code every time after the successful username/password authentication, while the others allow reusing the pin code of the previous session (the user is not asked to set up the pin code after the login if they have done it before for the same device)

Since I cannot find the standard, what I can do at best is to try reverse-engineer using my imagination to figure out the reason behind the flow. But I am no security expert, please feel free to share your experience or point out which parts are wrong.

## The Flow

1. At the most basic level, we have the username/password method, in which the server returns an `accessToken` to the app on a successful user's authentication. The app saves this token in the device's memory (RAM) and attaches it to every subsequent request to access authorized resources.

    ```mermaid
    sequenceDiagram
    
    User-->>Device: username/password
    Device->>Server: username/password
    Server->>Device: accessToken
    User-->>Device: perform actions
    Device->>Server: Authorization: bearer ${accessToken}
    ```

    [![enter image description here][1]][1]


2. But when the app is terminated and re-opened, the above flow alone must ask the user to re-enter username/password, which is not a good user experience, therefore we typically use a sort of `rememberMe` token (a.k.a `refreshToken` in OAuth). The server returns this token along with the `accessToken` on successful authentication. The app saves this token in the device's secured storage and uses it to obtain a new `accessToken` when the old one is not available or expired.

    ```mermaid
    sequenceDiagram
    
    User-->>Device: username/password
    Device->>Server: username/password
    Server->>Device: accessToken,refreshToken
    Device->>Device: persist refreshToken
    User-->>Device: perform actions
    Device->>Server: Authorization: bearer ${accessToken}
    User-->>User: Idle for a while
    User-->>Device: perform actions
    Device->>Server: renewToken (refreshToken)
    Server->>Device: accesToken,refreshToken
    Device->>Device: persist refreshToken
    Device->>Server: Authorization: bearer ${accessToken}
    ```

    [![enter image description here][2]][2]
   

3. But then, most financial apps have an additional security requirement to re-authenticate the user after a while (session timeout), or every time the user re-open the app. In the web application era, users don't have too much problem about having to re-enter the username/password because they don't use the app (website) that often. But that is not the case for mobile apps nowadays. Fortunately, *we more or less can assume that a mobile device is private to a user and can be used for authentication.*

    Here comes the pin code authentication. The app generate, upon installation, an app's specific universal unique id, call it a `deviceId`, save it in the device secured storage. Then after a successful authentication, the server returns a short-lived, one-time token just for setting up the pin code. The app asks the user the set up a 5-6 digits `pinCode`, then send the `pinCode` and `deviceId` to the server along with the one-time token, The server hash the combination of `pinCode` and `deviceId` and save the hash in a database table as one of the user's allowed devices, and then returns an `accessToken` to the app. Optionally, we can send an email to the user to allow the device explicitly.

    ```mermaid
    sequenceDiagram
    
    User-->>Device: Open app first time
    Device->>Device: generate and persist deviceId
    User-->>Device: username/password
    Device->>Server: username/password + deviceId
    Server->>Server: Validate username/password
    alt Optional
    Server->>Server: Send email asking the user to allow the device
    end
    Server->>Device: temporaryToken
    
    Device-->>User: Ask for pinCode
    User-->>Device: Enter a pinCode
    Device->>Server: pinCode + deviceId + temporaryToken
    Server->>Server: save hash of pinCode + deviceId
    alt Optional
    Server->>Server: save hash of deviceId to check if it is the same device in the next login
    end
    Server->>Device: accessToken
    User-->>User: Idle for a while
    User-->>Device: perform actions
    Device-->>User: ask for pinCode
    User-->>Device: Enter pinCode
    Device->>Server: renewToken (pinCode + deviceId)
    Server->>Device: accesToken
    
    ```

    [![enter image description here][3]][3]

    If my imagination so far is the correct reason for the pin code flow, I then can conclude that, in the variations **V1** above, the apps that allow the user to use the same `pinCode` across multiple devices are not using the pin code flow properly. It implies that they save the `pinCode` as plain text, or as a hash separated from the `deviceId` and it is no better than an alternative weaker password.

4. As an alternative to saving the hash of `pinCode` + `deviceId` on the server-side, we can use the `pinCode` to encrypt the `rememberToken` or the `refreshToken` of the typical OAuth flow, and save the encrypted result in the device's secured storage. When we want to re-authenticate the user, we ask the user the enter the `pinCode` and use it to decrypt the token and use the token to renew an `accessToken`. This is more secured because the `refreshToken` is not only specific to a device, but also specific to a user's authentication.

    ```mermaid
    sequenceDiagram
    
    User-->>Device: username/password
    Device->>Server: username/password
    Server->>Device: accessToken,refreshToken
    Device-->>User: ask for pinCode
    User-->>Device: enter a pinCode
    Device->>Device: persist encrypt(refreshToken,pinCode)
    User-->>User: Idle for a while
    User-->>Device: perform actions
    Device-->>User: Ask for pinCode
    User-->>Device: Enter a pinCode
    Device->>Device: refreshToken = decrypt(ciphertext, pinCode)
    Device->>Server: renewToken (refreshToken)
    Server->>Device: accesToken,refreshToken
    Device->>Device: persist encrypt(refreshToken,pinCode)
    Device->>Server: Authorization: bearer ${accessToken}
    ```

    [![enter image description here][4]][4]

    Again if this is correct, I can conclude that the variations in **V2** are both valid. We can choose to store and validate the secret at either the server or the device. If we do it at the device, we avoid the risk that it may be compromised at the server, but with the limitation that leads to the variations **V3**. We must ask the user to set up a new `pinCode` after login because we need it to encrypt the `refreshToken`. If we do it on the server, we have an option to let the user use the old `pinCode`. In this case, the server has to also keep the hash of the `deviceId` (in addition to the has of `deviceId` + `pinCode`) to be able to determine (without using `pinCode`) if the user is logged in on the same device. It is also possible to manage the devices such as blocking or removing from the allowed list


  [1]: https://i.stack.imgur.com/SH9NT.png
  [2]: https://i.stack.imgur.com/jr5D1.png
  [3]: https://i.stack.imgur.com/yFR84.png
  [4]: https://i.stack.imgur.com/InaP7.png