Skip to main content

Using OAuth 2.0 to Access Quran.Foundation APIs

Quran.Foundation User APIs use the OAuth 2.0 Authorization Code flow with PKCE for authenticating users and authorizing access to their data. This guide mirrors our Quick Start style: clear steps, tips, and ready-to-run examples in Python (requests) and JavaScript (Node + axios).

For identity details (ID token), see OpenID Connect.

πŸ”€ Flow split: Use Client Credentials for Content APIs (see Quick Start). Use Authorization Code (+PKCE) for User APIs (this page).


Overview​

Here's how the OAuth2 process works in a common scenario where user consent is needed to access their data:

oauth2 process

πŸ”§ Further examples: See our Example OAuth2 client
and the hosted demo here.


🧩 Step 1: Request OAuth 2.0 Client Credentials​

  1. Submit an Application to obtain your client_id and (for confidential apps) client_secret.
  2. Provide one or more exact redirect_uri values. These must match exactly at runtime.

⚠️ Never embed client_secret in public clients (SPA/mobile). Use PKCE and keep secrets on the server only.


πŸ”— Step 2: Build Authorization URL (with PKCE)​

Authorization endpoint: /oauth2/auth
Method: GET

Required query params:

  • response_type=code
  • client_id=YOUR_CLIENT_ID
  • redirect_uri=YOUR_REDIRECT_URI (must match exactly)
  • scope=openid offline user collection
  • state=RANDOM_STRING (validate on callback; CSRF)
  • nonce=RANDOM_STRING (strongly recommended when requesting openid; some deployments require it. If an ID token is returned, verify the token’s nonce claim matches your original value.)
  • code_challenge=BASE64URL_SHA256(code_verifier)
  • code_challenge_method=S256

Environment bases

  • Pre-Production: https://prelive-oauth2.quran.foundation
  • Production: https://oauth2.quran.foundation

Example URL (Pre-Production with PKCE):

https://prelive-oauth2.quran.foundation/oauth2/auth?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-app.com%2Foauth%2Fcallback
&scope=openid%20offline%20user%20collection
&state=VeimvfgqexjiCoCkRWSGcb333o3a
&nonce=RANDOM_NONCE_VALUE
&code_challenge=BASE64URL_SHA256_OF_CODE_VERIFIER
&code_challenge_method=S256

Least privilege: Add scopes only as you need them.

Tip: Persist your code_verifier server-side (session or secure httpOnly cookie) before redirecting to /oauth2/auth, then read it on the callback to redeem the code.

For example, request collection only when the user actually creates or manages a collection.

PKCE helpers (Node)
const crypto = require('crypto');

function base64url(buf) {
return buf.toString('base64').replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}

function generatePkcePair() {
const codeVerifier = base64url(crypto.randomBytes(32));
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
const codeChallenge = base64url(hash);
return { codeVerifier, codeChallenge };
}
PKCE helpers (Python)
import os, base64, hashlib

def base64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).decode().rstrip('=')

def generate_pkce_pair():
code_verifier = base64url(os.urandom(32))
code_challenge = base64url(hashlib.sha256(code_verifier.encode()).digest())
return code_verifier, code_challenge

πŸ”‘ Step 3: Exchange Code for Tokens​

  • Token endpoint: /oauth2/token
  • Method: POST
  • Public PKCE clients: send client_id in the body + code_verifier
  • Confidential clients: use HTTP Basic (client_id:client_secret)

Expected response includes access_token, refresh_token (if offline scope), id_token (if openid), expires_in.

Note: If you used PKCE in Step 2, always send code_verifier here.

cURL (confidential client)
curl --request POST \
--url https://prelive-oauth2.quran.foundation/oauth2/token \
--user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code&code=YOUR_CODE&redirect_uri=https%3A%2F%2Fyour-app.com%2Foauth%2Fcallback&code_verifier=YOUR_CODE_VERIFIER'
cURL (public PKCE client)
curl --request POST \
--url https://prelive-oauth2.quran.foundation/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=authorization_code&client_id=YOUR_CLIENT_ID&code=YOUR_CODE&redirect_uri=https%3A%2F%2Fyour-app.com%2Foauth%2Fcallback&code_verifier=YOUR_CODE_VERIFIER'
JavaScript (Node, axios β€” public PKCE)
const axios = require('axios');

async function exchangeCodeForTokens(clientId, code, redirectUri, codeVerifier) {
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('client_id', clientId);
params.append('code', code);
params.append('redirect_uri', redirectUri);
params.append('code_verifier', codeVerifier);

const res = await axios.post(
'https://prelive-oauth2.quran.foundation/oauth2/token',
params.toString(),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return res.data;
}
Python (requests β€” public PKCE)
import requests

def exchange_code_for_tokens(client_id, code, redirect_uri, code_verifier):
resp = requests.post(
'https://prelive-oauth2.quran.foundation/oauth2/token',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={
'grant_type': 'authorization_code',
'client_id': client_id,
'code': code,
'redirect_uri': redirect_uri,
'code_verifier': code_verifier,
},
)
return resp.json()

Sample token response

{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN_IF_OFFLINE_SCOPE",
"id_token": "JWT_IF_OPENID_SCOPE",
"scope": "openid offline user collection"
}

Verify granted scopes​

After the token exchange, compare the scope field in the token response with the features your app intends to enable; hide or disable any feature whose scope wasn’t granted.


🟒 Step 4: Call User APIs with Headers​

Include the token and your client ID in request headers:

x-auth-token: YOUR_ACCESS_TOKEN
x-client-id: YOUR_CLIENT_ID
cURL β€” Get collections
curl --request GET \
--url https://apis-prelive.quran.foundation/auth/v1/collections \
--header "x-auth-token: YOUR_ACCESS_TOKEN" \
--header "x-client-id: YOUR_CLIENT_ID"
JavaScript (Node)
const axios = require('axios');

async function getCollections(accessToken, clientId) {
const res = await axios.get(
'https://apis-prelive.quran.foundation/auth/v1/collections',
{ headers: { 'x-auth-token': accessToken, 'x-client-id': clientId } }
);
return res.data;
}
Python
import requests

def get_collections(access_token, client_id):
r = requests.get(
'https://apis-prelive.quran.foundation/auth/v1/collections',
headers={'x-auth-token': access_token, 'x-client-id': client_id}
)
return r.json()

πŸ” Step 5: Refresh the Access Token (offline scope)​

When expires_in elapses, exchange the refresh_token for a new access_token.

cURL (confidential)
curl --request POST \
--url https://prelive-oauth2.quran.foundation/oauth2/token \
--user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'
cURL (public PKCE)
curl --request POST \
--url https://prelive-oauth2.quran.foundation/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token&client_id=YOUR_CLIENT_ID&refresh_token=YOUR_REFRESH_TOKEN'
JavaScript (Node)
const axios = require('axios');

async function refreshAccessToken(clientId, clientSecret, refreshToken) {
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('refresh_token', refreshToken);

const res = await axios.post(
'https://prelive-oauth2.quran.foundation/oauth2/token',
params.toString(),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
auth: { username: clientId, password: clientSecret }
}
);
return res.data;
}
Python
import requests

def refresh_access_token(client_id, client_secret, refresh_token):
resp = requests.post(
'https://prelive-oauth2.quran.foundation/oauth2/token',
auth=(client_id, client_secret),
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={'grant_type': 'refresh_token', 'refresh_token': refresh_token}
)
return resp.json()

⚠️ Important Considerations​

  • Redirect URIs: Must match exactly (including scheme, host, path, and trailing slash).
  • State: Always send and validate state to protect against CSRF.
  • Nonce: When you request openid, always include a nonce. Some deployments enforce it; if an ID token is issued, you must verify the nonce claim equals the value you initially sent.
  • PKCE: Use for public clients (SPAs/mobile). Do not ship client_secret to browsers/mobile apps.
  • Scopes: Use openid offline user collection. Add more scopes only as needed.
  • Scopes note: Quran.Foundation expects offline (not offline_access).
  • Token storage: Store refresh_token securely; rotate if compromised.
  • Clock skew: Ensure system time is correct (e.g., via NTP) to avoid invalid_grant due to time drift.

🚩 Do not mix tokens across environments. A token from Pre-Production will not work on Production, and vice versa.

EnvironmentAuth URLAPI Base URLUsage
Pre-Productionhttps://prelive-oauth2.quran.foundationhttps://apis-prelive.quran.foundationTesting & development
Productionhttps://oauth2.quran.foundationhttps://apis.quran.foundationLive applications

❌ Common Issues & Troubleshooting​

ErrorLikely CauseResolution
invalid_clientWrong client credentialsVerify client_id/secret, environment
invalid_grantBad/expired/used code or refresh_tokenEnsure one-time use, check clock skew
redirect_uri_mismatchRedirect URI not exact matchAlign with registered value
invalid_scopeScope misspelled or not allowedUse valid scopes; request incrementally
401 Unauthorized (APIs)Missing/expired tokenSend x-auth-token and x-client-id, refresh token