Secure Authentication and Authorisation in React Native
This article offers a detailed overview of a talk I presented during the February edition of the LeedsJS meet-up event.
Introduction
In the realm of mobile app development, security is a crucial but frequently underestimated aspect, often overshadowed by concerns about backend security. Contrary to popular belief, the security of the frontend, particularly authentication and authorisation mechanisms, is equally vital. This article delves into the often-overlooked realm of securing mobile applications, with a keen focus on the fundamental aspects of authentication and authorisation.
Authentication vs Authorisation
Authentication and authorisation are two fundamental concepts in computer security, often used together to control access to resources. While they are related, they serve distinct purposes:
Authentication according to OWASP (Open Web Application Security Project) is the process of verifying that an individual, entity, or website is who/what it claims to be. It typically involves providing credentials such as usernames, passwords, security tokens, biometric data, or digital certificates. The main goal of authentication is to establish trust in the identity of the entity requesting access.
Authorisation, also known as access control, is how an application grants access to content and functions to some users and not to others. These checks are done after authentication, and govern what authenticated users can do.
Understanding React Native Architecture
React Native, at its core, operates through a JavaScript thread that serves as the foundation for application logic and user interface rendering. This JavaScript thread interacts with the native components of the target platform, whether iOS or Android, via a bridge mechanism. This bridge facilitates seamless two-way communication, allowing JavaScript code to access native modules and APIs (Application Programming Interfaces), while also enabling native code to invoke JavaScript functions. However, this bridge introduces potential security vulnerabilities, necessitating careful consideration and mitigation strategies to safeguard against potential threats.
General Approach to Securing Apps and Best Practices
To implement secure authentication and authorisation in a React Native application, it is typical to use a combination of techniques such as token-based authentication, secure storage, and server-side verification. A general approach to this involves the following:
Token-based authentication
This is the use of tokens (such as JSON Web Tokens - JWT) for authentication. When a user logs in, the server issues a token to the client which is to be stored securely and passed with each request. This leads to the next point.
Secure storage
React Native offers AsyncStorage; a simple, synchronous, persistent key-value storage as an option for local storage. However, while it is convenient for storing small pieces of data like user preferences or settings, it is not secure for sensitive data like tokens. It was not designed with security as its primary focus, data stored in AsyncStorage is not encrypted by default, and it can be accessed by JavaScript code running in the same context. The lack of encryption and potential for data leakage are critical no-gos in security.
KeyChain(iOS) and Keystore(Android) are secure storage mechanisms provided by Apple and Android respectively to handle passwords, cryptographic keys, and other sensitive data. These also employ hardware backed encryption providing a high level of security.
There are libraries such as react-native-keychain or rn-secure-storage that will allow developers securely store data through KeyChain or EncryptedSharedPreferences in Android and may add a further layer of protection by adding AES-256 encryption.
Authentication Flow and Authorisation
The authentication flow commonly consists of, but is not limited to registration, login, logout, and password reset functionalities. In the past, it was not uncommon for developers to write all the logic required to provide these functions from scratch, however, attempting to do so now can leave the app vulnerable to attacks, especially man-in-the-middle (MitM) attacks. It is very advisable to use established protocols like OAuth 2.0 and OpenID Connect as these protect your app from a plethora of attacks or vulnerabilities such as brute force, session hijacking and insufficient access controls.
An important thing to note in React Native is that when using OAuth 2.0, it is particularly helpful to include the use of Proof of Key Code Exchange (PKCE pronounced ‘pixy’). PKCE uses the SHA-256 algorithm to securely encrypt the code verifier as an added layer of security with the aim to prevent authorisation code interception attacks. This is needed because the default redirects used in web apps are not secure for mobile because of the absence of a centralised method of registering URL schemes.
import React, { useState, useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import { authorize, refresh, revoke } from 'react-native-app-auth';
const config = {
issuer: 'https://your-oidc-provider.com',
clientId: 'your-client-id',
redirectUrl: 'your-redirect-url',
scopes: ['openid', 'profile', 'email'],
usePKCE: true, // Include this property to enable PKCE
};
const App = () => {
const [authState, setAuthState] = useState(null);
useEffect(() => {
// Check if user is already authenticated
authorize(config)
.then((authState) => {
setAuthState(authState);
})
.catch((error) => console.error('Failed to authorize:', error));
}, []);
const signIn = async () => {
try {
const authState = await authorize(config);
setAuthState(authState);
} catch (error) {
console.error('Failed to authorize:', error);
}
};
const signOut = async () => {
try {
await revoke(config, { tokenToRevoke: authState.accessToken, sendClientId: true });
setAuthState(null);
} catch (error) {
console.error('Failed to revoke token:', error);
}
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{authState ? (
<View>
<Text>Welcome, {authState.accessTokenPayload.name}!</Text>
<Button title="Sign Out" onPress={signOut} />
</View>
) : (
<Button title="Sign In" onPress={signIn} />
)}
</View>
);
};
export default App;
In this example, the react-native-app-auth
library is used to perform OAuth authorisation with an OpenID Connect provider in React Native.
- By setting
usePKCE
totrue
, the library will automatically generate a code verifier and challenge for PKCE, which will be used in the authorisation code flow. - The
authorize
function initiates the authentication flow, while therevoke
function is used to sign out and revoke the access token.
This code demonstrates how using a library can simplify the implementation of OAuth authentication in a React Native application.
Error handling and Token refresh
Ensure the app handles authentication and authorisation errors smoothly. If a token expires or becomes invalid, prompt users to log in again. Similarly, if users try to access unauthorised features, show them a relevant message. Set up a token refresh mechanism to renew tokens automatically before they expire, providing users with a seamless experience and reducing the need for frequent logins.
Backend security and Secure communication
React Native, like many other mobile development frameworks, is susceptible to man-in-the-middle (MitM) attacks primarily due to the fact that it relies on network communication to fetch resources and data from external servers. This is why it is very important to secure your backend API and communication channels by implementing industry-standard security practices. This includes using HTTPS (HyperText Transfer Protocol Secure), performing input validation, and ensuring proper authentication and authorisation checks on the server side.
Encrypt sensitive data transmitted between your app and server to prevent interception. Avoid transmitting sensitive information in plain text over the network to enhance security.
This adheres to 1.2.4 of the Authentication Architectural Requirements as contained in the OWASP Application Security Verification Standard(ASVS). As a rule of thumb, it is helpful to use this resource as a guideline in ensuring the security of your applications.
Testing
Thoroughly test your authentication and authorisation flows to detect and address any security vulnerabilities. Conduct both manual and automated testing, including penetration testing if feasible, to ensure the robustness of your security measures.
Keeping libraries updated
Ensure your dependencies, including authentication and security-related libraries, are kept up-to-date by performing regular updates to patch any known vulnerabilities.
By employing these best practices, you can establish secure authentication and authorisation within your React Native application, safeguarding user data and thwarting unauthorised access.
Conclusion
According to Zimperium’s 2023 Global Threat Report, there has been a 138% year-over-year increase in critical Android vulnerabilities discovered and 14% of mobile apps using cloud storage were vulnerable due to unsecured configurations.
With over 30k apps spanning across 60 billion downloads, securing React Native applications is paramount to safeguarding user data and ensuring trust in our digital experiences.
By embracing a proactive approach to authentication security, we empower users to engage with our applications confidently, knowing that their data is safeguarded with the highest standards of protection. Let us continue to iterate, innovate, and collaborate in our journey towards building a more secure digital ecosystem for all.
Thank you.