React Native
We've built this for the Ummah so you don't have to. By using our OAuth2, you get:
- Zero user management — No database, no password resets, no account recovery
- Cross-app sync — Users' bookmarks, goals, and streaks sync with Quran.com automatically
- Single Sign-On — One login works across all Quran apps
Please make sure you read our User Related APIs Quickstart Guide or the full integration guide.
Obtaining OAuth 2.0 client credentials
A prerequisite to creating client credentials is for your app to have the value of the redirect URL where the user will land after successfully authenticating/logging out implemented.
Once you have the redirect URL, create your OAuth2 app in Developer Console and register it there.
If you select Backend/server app, keep the login screen and PKCE flow in your mobile app, but perform the authorization-code exchange and refresh-token exchange on your backend. If you select Frontend or mobile app, use a fully public in-app token exchange flow without a client secret.
Choose the flow that matches the app type you selected in Developer Console:
- Frontend or mobile app: use a public client. Generate PKCE and exchange the code directly in the app; no client secret is involved.
- Backend/server app: use a confidential client. Let the app generate PKCE, then send
code + code_verifier + redirect_urito your backend for token exchange and refresh.
The example below demonstrates the Backend/server app flow. Public clients should use an OIDC-capable library with PKCE and secure platform storage as described in the public-client section below.
⚡ Quick Setup
npx expo install expo-auth-session expo-web-browser expo-crypto
🚀 Confidential Backend Example
Pair this App.js client with your backend exchange, refresh, logout, and User API proxy routes:
import * as React from "react";
import { Button, Text, View, StyleSheet } from "react-native";
import { useAuthRequest } from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
WebBrowser.maybeCompleteAuthSession();
// ⚙️ Configuration - Update these values!
const CLIENT_ID = "YOUR_CLIENT_ID"; // From your OAuth application
const REDIRECT_URI = "exp://192.168.x.x:8081"; // Your Expo dev URL or custom scheme
const BACKEND_BASE_URL = "https://your-backend.example.com";
const USE_PRELIVE = true; // set to false for production
const authBaseUrl = USE_PRELIVE
? "https://prelive-oauth2.quran.foundation"
: "https://oauth2.quran.foundation";
// OAuth2 endpoints
const discovery = {
authorizationEndpoint: `${authBaseUrl}/oauth2/auth`,
tokenEndpoint: `${authBaseUrl}/oauth2/token`,
revocationEndpoint: `${authBaseUrl}/oauth2/revoke`,
};
export default function App() {
const [authSession, setAuthSession] = React.useState(null);
const [request, response, promptAsync] = useAuthRequest(
{
clientId: CLIENT_ID,
scopes: ["openid", "offline_access", "bookmark", "collection", "user"],
redirectUri: REDIRECT_URI,
usePKCE: true,
},
discovery
);
React.useEffect(() => {
const exchangeOnBackend = async () => {
if (!response) {
return;
}
if (response.error) {
console.error("Auth error:", response.params.error_description);
return;
}
if (response.type !== "success" || !request?.codeVerifier) {
return;
}
try {
const backendResponse = await fetch(
`${BACKEND_BASE_URL}/api/auth/qf/exchange`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
code: response.params.code,
codeVerifier: request.codeVerifier,
redirectUri: REDIRECT_URI,
}),
}
);
const payload = await backendResponse.json();
if (!backendResponse.ok) {
throw new Error(payload.error || "Token exchange failed");
}
if (!payload.user) {
throw new Error("Backend did not return a verified user profile");
}
setAuthSession({ userProfile: payload.user });
} catch (error) {
console.error("Token exchange failed:", error);
}
};
exchangeOnBackend();
}, [request?.codeVerifier, response]);
const logout = async () => {
await fetch(`${BACKEND_BASE_URL}/api/auth/qf/logout`, {
method: "POST",
credentials: "include",
}).catch(() => {});
setAuthSession(null);
};
// Logged in view
if (authSession) {
const { userProfile } = authSession;
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome, {userProfile.first_name || userProfile.name || "User"}!
</Text>
<Text style={styles.email}>{userProfile.email}</Text>
<View style={styles.buttonContainer}>
<Button title="Logout" onPress={logout} />
</View>
</View>
);
}
// Login view
return (
<View style={styles.container}>
<Text style={styles.title}>Quran App</Text>
<View style={styles.buttonContainer}>
<Button
disabled={!request}
title="Login with Quran.com"
onPress={() => promptAsync()}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
title: { fontSize: 28, fontWeight: "bold", marginBottom: 40 },
welcome: { fontSize: 24, fontWeight: "600", marginBottom: 10 },
email: { fontSize: 16, color: "#666", marginBottom: 30 },
buttonContainer: { marginTop: 20, width: "80%" },
});
Frontend or Mobile App Variant
For the direct public-client flow, keep PKCE exchange and refresh in the app with no client secret. Request offline_access when refresh is needed, validate the ID-token signature, issuer, audience, expiry, and nonce with an OIDC-capable library before using its claims, and keep tokens in expo-secure-store. Never print token responses or authorization codes to the console.
npx expo install expo-secure-store
Expected Screens



🔑 Making API Calls
For the confidential backend/server pattern above, proxy User API calls through your backend session rather than returning user tokens to the app:
// Example: Fetch user's bookmarks
const fetchBookmarks = async () => {
const response = await fetch(
`${BACKEND_BASE_URL}/api/qf/bookmarks`,
{
credentials: "include",
}
);
return response.json();
};
🔄 Token Refresh
Access tokens expire after 1 hour. For confidential backend/server apps, keep the refresh token in the backend session and refresh there:
const refreshTokens = async () => {
const response = await fetch(
`${BACKEND_BASE_URL}/api/auth/qf/refresh`,
{
method: "POST",
credentials: "include",
}
);
const payload = await response.json();
setAuthSession({ userProfile: payload.user });
};
Public Frontend or mobile app clients should use expo-secure-store for locally held refresh tokens. Confidential Backend/server app clients should keep refresh tokens in the server session instead:
import * as SecureStore from "expo-secure-store";
const persistRefreshToken = (refreshToken) =>
SecureStore.setItemAsync("qf.refreshToken", refreshToken);