DEV Community

Pipat Sampaokit
Pipat Sampaokit

Posted on • Edited on

Reverse-engineering the pin code authentication flow for mobile apps

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.

    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

  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.

    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

  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.

    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

    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.

    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

    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

Top comments (0)