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
-
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}
-
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.arefreshToken
in OAuth). The server returns this token along with theaccessToken
on successful authentication. The app saves this token in the device's secured storage and uses it to obtain a newaccessToken
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}
-
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 digitspinCode
, then send thepinCode
anddeviceId
to the server along with the one-time token, The server hash the combination ofpinCode
anddeviceId
and save the hash in a database table as one of the user's allowed devices, and then returns anaccessToken
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
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 thepinCode
as plain text, or as a hash separated from thedeviceId
and it is no better than an alternative weaker password. -
As an alternative to saving the hash of
pinCode
+deviceId
on the server-side, we can use thepinCode
to encrypt therememberToken
or therefreshToken
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 thepinCode
and use it to decrypt the token and use the token to renew anaccessToken
. This is more secured because therefreshToken
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}
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 therefreshToken
. If we do it on the server, we have an option to let the user use the oldpinCode
. In this case, the server has to also keep the hash of thedeviceId
(in addition to the has ofdeviceId
+pinCode
) to be able to determine (without usingpinCode
) 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)