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, please submit your OAuth2 Application.

For most clients issued through Request Access, 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 Quran Foundation provisioned your client with token_endpoint_auth_method=none, you can also use a fully public in-app token exchange flow.

Recommended Pattern

Let the app open the Quran Foundation login screen and generate PKCE, then send code + code_verifier + redirect_uri to your backend. Your backend performs token exchange and refresh. If Quran Foundation provisioned your client with token_endpoint_auth_method=none, you can also do the token exchange directly in the app.

⚡ Quick Setup

npx expo install expo-auth-session expo-web-browser expo-crypto
npm install jwt-decode

🚀 Complete Working Example

Copy this into your App.js to get OAuth2 working immediately:

import * as React from "react";
import { Button, Text, View, StyleSheet } from "react-native";
import jwtDecode from "jwt-decode";
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" },
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");
}

const userProfile = payload.user || jwtDecode(payload.idToken);
setAuthSession({ ...payload, userProfile });
} 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",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: authSession?.refreshToken }),
}).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%" },
});

Example Repository

We have created a complete Example OAuth2 React Native client that you can clone and run. That repository shows the direct public-client exchange variant. For current Request Access clients, keep the in-app login pieces and move token exchange to your backend as shown above.

User is not logged in yetUser is consentingUser is logged in

🔑 Making API Calls

Once authenticated, use the accessToken to call User APIs:

// Example: Fetch user's bookmarks
const fetchBookmarks = async () => {
const response = await fetch(
"https://apis.quran.foundation/auth/v1/bookmarks",
{
headers: {
"x-auth-token": authSession.accessToken,
"x-client-id": CLIENT_ID,
},
}
);
return response.json();
};

🔄 Token Refresh

Access tokens expire after 1 hour. For most Request Access clients, refresh them through your backend:

const refreshTokens = async () => {
const response = await fetch(
`${BACKEND_BASE_URL}/api/auth/qf/refresh`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: authSession.refreshToken }),
}
);

const payload = await response.json();
setAuthSession((current) => ({ ...current, ...payload }));
};
Store Refresh Token Securely

If your app keeps refresh tokens locally, use expo-secure-store to persist them between app sessions. If you already have backend sessions, keep the refresh token on the server instead:

import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("refreshToken", authSession.refreshToken);