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:
π§ Further examples: See our Example OAuth2 client
and the hosted demo here.
π§© Step 1: Request OAuth 2.0 Client Credentialsβ
- Submit an Application to obtain your
client_id
and (for confidential apps)client_secret
. - 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 requestingopenid
; some deployments require it. If an ID token is returned, verify the tokenβsnonce
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 thecode
.
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 anonce
. Some deployments enforce it; if an ID token is issued, you must verify thenonce
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
(notoffline_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.
Environment | Auth URL | API Base URL | Usage |
---|---|---|---|
Pre-Production | https://prelive-oauth2.quran.foundation | https://apis-prelive.quran.foundation | Testing & development |
Production | https://oauth2.quran.foundation | https://apis.quran.foundation | Live applications |
β Common Issues & Troubleshootingβ
Error | Likely Cause | Resolution |
---|---|---|
invalid_client | Wrong client credentials | Verify client_id/secret , environment |
invalid_grant | Bad/expired/used code or refresh_token | Ensure one-time use, check clock skew |
redirect_uri_mismatch | Redirect URI not exact match | Align with registered value |
invalid_scope | Scope misspelled or not allowed | Use valid scopes; request incrementally |
401 Unauthorized (APIs) | Missing/expired token | Send x-auth-token and x-client-id , refresh token |