Skip to main content

React Native

🤲 Why Use Quran Foundation Authentication?

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

Learn more about the benefits →

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.

Recommended Pattern

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_uri to 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

User is not logged in yetUser is consentingUser is logged in

🔑 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 });
};
Store Refresh Token Securely

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);