Skip to main content

⚡ User APIs Quick Start

Get user authentication working in under 5 minutes. This guide shows you the fastest path to integrating Quran Foundation OAuth2 for accessing User APIs (bookmarks, collections, reading progress, etc.).

Recommended for first-time users

We've built this for the Ummah so you don't have to. Use our auth and get cross-app sync with Quran.com for free — while keeping full freedom to build your own features. Link our user.sub to your database for custom logic. Learn more →

🤖 Using AI to code? Look for the "🤖 AI prompt" sections throughout this guide — copy-paste them into ChatGPT, Claude, or Copilot for instant implementation help!


📋 Prerequisites

  1. Get OAuth2 credentials: Request Access →
  2. Register your redirect URI (e.g., http://localhost:3000/callback or yourapp://callback)

🚀 Choose Your Platform

React Native (Expo)

The fastest way to add authentication to a React Native app:

npx expo install expo-auth-session expo-web-browser expo-crypto
npm install jwt-decode
import * as React from "react";
import { Button, Text, View } from "react-native";
import jwtDecode from "jwt-decode";
import {
useAuthRequest,
exchangeCodeAsync,
revokeAsync,
} from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";

WebBrowser.maybeCompleteAuthSession();

// OAuth2 endpoints - use prelive for testing, production for live apps
const discovery = {
authorizationEndpoint: "https://oauth2.quran.foundation/oauth2/auth",
tokenEndpoint: "https://oauth2.quran.foundation/oauth2/token",
revocationEndpoint: "https://oauth2.quran.foundation/oauth2/revoke",
};

export default function App() {
const [user, setUser] = React.useState(null);
const [tokens, setTokens] = React.useState(null);

const [request, response, promptAsync] = useAuthRequest(
{
clientId: "YOUR_CLIENT_ID", // Replace with your client ID
scopes: ["openid", "offline_access", "bookmark", "collection", "user"],
redirectUri: "yourapp://callback", // Replace with your redirect URI
usePKCE: true, // Required for mobile apps
},
discovery
);

React.useEffect(() => {
if (response?.type === "success") {
const { code } = response.params;

// Exchange code for tokens
exchangeCodeAsync(
{
clientId: "YOUR_CLIENT_ID",
code,
redirectUri: "yourapp://callback",
extraParams: { code_verifier: request.codeVerifier },
},
discovery
).then((tokenResponse) => {
setTokens(tokenResponse);
// Decode ID token to get user info
const userInfo = jwtDecode(tokenResponse.idToken);
setUser(userInfo);
});
}
}, [response]);

const logout = async () => {
if (tokens?.refreshToken) {
await revokeAsync(
{ clientId: "YOUR_CLIENT_ID", token: tokens.refreshToken },
discovery
);
}
setUser(null);
setTokens(null);
};

if (user) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={{ fontSize: 24, marginBottom: 20 }}>
Welcome, {user.first_name || user.name || "User"}!
</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}

return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Button
disabled={!request}
title="Login with Quran.com"
onPress={() => promptAsync()}
/>
</View>
);
}

👉 Full React Native Example Repo

🤖 AI prompt: implement React Native OAuth2
Implement Quran Foundation OAuth2 authentication for React Native (Expo).

Goal
- Add "Login with Quran.com" button that authenticates users via OAuth2
- Use PKCE flow (required for mobile apps - no client_secret needed)
- Store tokens and decode user info from ID token

Dependencies
- expo-auth-session
- expo-web-browser
- expo-crypto
- jwt-decode

OAuth2 Endpoints (production)
- Authorization: https://oauth2.quran.foundation/oauth2/auth
- Token: https://oauth2.quran.foundation/oauth2/token
- Revocation: https://oauth2.quran.foundation/oauth2/revoke

Implementation requirements
- Use useAuthRequest hook with usePKCE: true
- Scopes: openid, offline_access, bookmark, collection, user
- On success, exchange code for tokens using exchangeCodeAsync
- Include code_verifier from request in extraParams
- Decode idToken with jwt-decode to get user info
- Implement logout that revokes refresh token

Acceptance checklist
- Login redirects to Quran.Foundation auth page
- After consent, user info is displayed
- Logout clears tokens and revokes refresh token
- No client_secret is used (PKCE only)

Web (Node.js + Express)

npm install express express-session simple-oauth2 jsonwebtoken dotenv
// server.js
const express = require("express");
const session = require("express-session");
const crypto = require("crypto");
const { AuthorizationCode } = require("simple-oauth2");
const jwt = require("jsonwebtoken");
require("dotenv").config();

const app = express();

app.use(
session({
secret: process.env.SESSION_SECRET || "your-secret",
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === "production" },
})
);

const oauth2Client = new AuthorizationCode({
client: {
id: process.env.CLIENT_ID,
secret: process.env.CLIENT_SECRET,
},
auth: {
tokenHost: "https://oauth2.quran.foundation",
tokenPath: "/oauth2/token",
authorizePath: "/oauth2/auth",
},
});

const CALLBACK_URL = process.env.BASE_URL + "/callback";

// Login route - redirects to Quran.Foundation
app.get("/login", (req, res) => {
// Generate cryptographically random state for CSRF protection
const state = crypto.randomBytes(32).toString("hex");
req.session.oauthState = state;

const authUrl = oauth2Client.authorizeURL({
redirect_uri: CALLBACK_URL,
scope: "openid offline_access bookmark collection user",
state,
});
res.redirect(authUrl);
});

// OAuth callback - exchanges code for tokens
app.get("/callback", async (req, res) => {
const { code, state } = req.query;

// Validate state parameter to prevent CSRF attacks
if (!state || state !== req.session.oauthState) {
console.error("State mismatch - possible CSRF attack");
return res.status(403).send("Invalid state parameter");
}

// Clear the stored state after validation
delete req.session.oauthState;

try {
const tokenResponse = await oauth2Client.getToken({
code,
redirect_uri: CALLBACK_URL,
});

// Store tokens in session
req.session.tokens = tokenResponse.token;

// Decode ID token for user info
const user = jwt.decode(tokenResponse.token.id_token);
req.session.user = user;

res.redirect("/");
} catch (error) {
console.error("OAuth Error:", error);
res.status(500).send("Authentication failed");
}
});

// Protected route example
app.get("/", (req, res) => {
if (!req.session.user) {
return res.send('<a href="/login">Login with Quran.com</a>');
}
res.send(
`Welcome, ${
req.session.user.first_name || req.session.user.name
}! <a href="/logout">Logout</a>`
);
});

// Logout
app.get("/logout", (req, res) => {
const logoutUrl = `https://oauth2.quran.foundation/oauth2/sessions/logout?client_id=${process.env.CLIENT_ID}`;
req.session.destroy();
res.redirect(logoutUrl);
});

app.listen(3000, () => console.log("Server running on http://localhost:3000"));

.env file:

CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
BASE_URL=http://localhost:3000
SESSION_SECRET=random-secure-string

👉 Full Web Example Repo

🤖 AI prompt: implement Node.js Express OAuth2
Implement Quran Foundation OAuth2 authentication for Node.js + Express.

Goal
- Add "Login with Quran.com" that redirects to OAuth2 authorization
- Handle callback, exchange code for tokens, store in session
- Decode ID token for user info
- Implement secure logout

Dependencies
- express
- express-session
- simple-oauth2
- jsonwebtoken
- crypto (built-in)

OAuth2 Configuration (production)
- Token Host: https://oauth2.quran.foundation
- Token Path: /oauth2/token
- Authorize Path: /oauth2/auth
- Logout: /oauth2/sessions/logout?client_id=YOUR_CLIENT_ID

Environment variables
- CLIENT_ID
- CLIENT_SECRET
- BASE_URL (e.g., http://localhost:3000)
- SESSION_SECRET

Implementation requirements
- Generate cryptographically random state (crypto.randomBytes) for CSRF protection
- Store state in session before redirect
- Validate state on callback (reject if mismatch)
- Use simple-oauth2 AuthorizationCode client
- Scopes: openid offline_access bookmark collection user
- Store tokens in session after exchange
- Decode id_token with jsonwebtoken to get user info
- Logout must destroy session AND redirect to OAuth2 provider logout

Security checklist
- State parameter generated and validated (CSRF protection)
- Session cookie secure in production
- Never log client_secret or tokens

iOS Native (Swift)

Use AppAuth-iOS for native iOS apps:

import AppAuth

let issuer = URL(string: "https://oauth2.quran.foundation")!
let clientId = "YOUR_CLIENT_ID"
let redirectUri = URL(string: "com.yourapp://callback")!

// Discover OAuth2 configuration
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { config, error in
guard let config = config else { return }

let request = OIDAuthorizationRequest(
configuration: config,
clientId: clientId,
scopes: [OIDScopeOpenID, "offline_access", "bookmark", "collection"],
redirectURL: redirectUri,
responseType: OIDResponseTypeCode,
additionalParameters: nil
)

// Present authorization flow
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
byPresenting: request,
presenting: self
) { authState, error in
if let authState = authState {
// Store authState.lastTokenResponse.accessToken
// Use for API calls
}
}
}

👉 iOS Guide with Demo

🤖 AI prompt: implement iOS OAuth2 with AppAuth
Implement Quran Foundation OAuth2 authentication for iOS using AppAuth.

Goal
- Add "Login with Quran.com" that opens OAuth2 flow
- Use AppAuth-iOS library for secure PKCE flow
- Handle callback and store auth state

Dependency
- AppAuth-iOS (via SPM or CocoaPods)

OAuth2 Issuer
- https://oauth2.quran.foundation

Implementation requirements
- Use OIDAuthorizationService.discoverConfiguration to fetch OAuth2 config
- Create OIDAuthorizationRequest with:
- clientId (no client_secret for mobile)
- scopes: OIDScopeOpenID, offline_access, bookmark, collection
- redirectURL: com.yourapp://callback (custom scheme)
- responseType: OIDResponseTypeCode
- Use OIDAuthState.authState(byPresenting:) to present flow
- Store authState securely (Keychain recommended)
- Access token via authState.lastTokenResponse.accessToken

Info.plist requirements
- Add URL scheme for redirect URI
- Add LSApplicationQueriesSchemes if needed

Acceptance checklist
- App opens Safari/ASWebAuthenticationSession for login
- Callback redirects back to app
- Access token is available for API calls
- Auth state persists across app launches

Android Native (Kotlin)

Use AppAuth-Android for native Android apps:

val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://oauth2.quran.foundation/oauth2/auth"),
Uri.parse("https://oauth2.quran.foundation/oauth2/token")
)

val authRequest = AuthorizationRequest.Builder(
serviceConfig,
"YOUR_CLIENT_ID",
ResponseTypeValues.CODE,
Uri.parse("com.yourapp://callback")
).setScopes("openid", "offline_access", "bookmark", "collection")
.build()

val authService = AuthorizationService(context)
val authIntent = authService.getAuthorizationRequestIntent(authRequest)
startActivityForResult(authIntent, AUTH_REQUEST_CODE)

👉 Android Guide with Demo

🤖 AI prompt: implement Android OAuth2 with AppAuth
Implement Quran Foundation OAuth2 authentication for Android using AppAuth.

Goal
- Add "Login with Quran.com" that opens OAuth2 flow
- Use AppAuth-Android library for secure PKCE flow
- Handle callback and store tokens

Dependency
- net.openid:appauth (via Gradle)

OAuth2 Endpoints
- Authorization: https://oauth2.quran.foundation/oauth2/auth
- Token: https://oauth2.quran.foundation/oauth2/token

Implementation requirements
- Create AuthorizationServiceConfiguration with endpoints
- Build AuthorizationRequest with:
- clientId (no client_secret for mobile)
- ResponseTypeValues.CODE
- redirectUri: com.yourapp://callback (custom scheme)
- scopes: openid, offline_access, bookmark, collection
- Use AuthorizationService to get auth intent
- Start activity for result
- In onActivityResult, exchange code for tokens
- Store tokens securely (EncryptedSharedPreferences recommended)

AndroidManifest requirements
- Add intent-filter for redirect URI scheme
- Add net.openid.appauth.RedirectUriReceiverActivity

Acceptance checklist
- App opens Chrome Custom Tab for login
- Callback redirects back to app
- Access token is available for API calls
- Tokens persist securely across app launches

🔑 Making Authenticated API Calls

Once you have an access_token, use it to call User APIs:

// Example: Get user's bookmarks
const response = await fetch(
"https://apis.quran.foundation/auth/v1/bookmarks",
{
headers: {
"x-auth-token": accessToken,
"x-client-id": "YOUR_CLIENT_ID",
},
}
);
const bookmarks = await response.json();
# Example: Get user's bookmarks
import requests

response = requests.get(
'https://apis.quran.foundation/auth/v1/bookmarks',
headers={
'x-auth-token': access_token,
'x-client-id': 'YOUR_CLIENT_ID',
}
)
bookmarks = response.json()

📚 Available Scopes

Request only the scopes you need:

ScopeAccess To
openidUser identity (ID token)
offline_accessRefresh tokens for long-lived sessions
userUser profile information
bookmarkUser's bookmarked verses
collectionUser's verse collections
reading_sessionReading progress and history
preferenceUser preferences and settings
goalReading goals
streakReading streaks

🔄 Token Refresh

Token Lifetimes

Token TypeLifetimeNotes
access_token1 hourUse for API calls
refresh_tokenLong-livedUsed to get new access tokens
id_token1 hourContains user identity (sub)
Refresh Token Policy
  • Refresh tokens are issued when you request the offline_access scope
  • Refresh tokens do not rotate on use (you can reuse the same refresh token)
  • Refresh tokens remain valid until explicitly revoked or user logs out
  • Store refresh tokens securely server-side :::

Use the refresh token to get a new access token:

// Using simple-oauth2
const newToken = await oauth2Client.refreshToken.refresh(tokenResponse);
# Using requests
response = requests.post(
'https://oauth2.quran.foundation/oauth2/token',
auth=(client_id, client_secret),
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
}
)

⚠️ Common Mistakes

Embedding client_secret in mobile/browser apps — Use PKCE instead, no secret needed ❌ Not validating state parameter — Always validate to prevent CSRF attacks ❌ Forgetting to store refresh_token — You need it for long-lived sessions ❌ Requesting too many scopes — Only request what you actually use ❌ Mixing prelive/production URLs — Tokens are environment-specific :::


🎯 Next Steps


💬 Need Help?

📧 [email protected] — We're here to help you integrate!