⚡ 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.).
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
- Get OAuth2 credentials: Request Access →
- Register your redirect URI (e.g.,
http://localhost:3000/callbackoryourapp://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
🤖 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
}
}
}
🤖 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)
🤖 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:
| Scope | Access To |
|---|---|
openid | User identity (ID token) |
offline_access | Refresh tokens for long-lived sessions |
user | User profile information |
bookmark | User's bookmarked verses |
collection | User's verse collections |
reading_session | Reading progress and history |
preference | User preferences and settings |
goal | Reading goals |
streak | Reading streaks |
🔄 Token Refresh
Token Lifetimes
| Token Type | Lifetime | Notes |
|---|---|---|
access_token | 1 hour | Use for API calls |
refresh_token | Long-lived | Used to get new access tokens |
id_token | 1 hour | Contains user identity (sub) |
- Refresh tokens are issued when you request the
offline_accessscope - 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
- Full OAuth2 Guide — Detailed step-by-step with AI prompts
- User APIs Reference — All available endpoints
- OpenID Connect — Understanding ID tokens
💬 Need Help?
📧 [email protected] — We're here to help you integrate!