Skip to main content

Manual OAuth2/OIDC Implementation

Quick Summary

What: Manual OAuth2 Authorization Code flow with PKCE for Quran Foundation User APIs. Who: Developers implementing custom auth flows, mobile flows, non-JS stacks, or OAuth2 debugging. Prerequisites: A client_id from Request Access. Time: ~30 minutes to implement. Result: Your app can authenticate users and access their Quran.com synced data.

Quran.Foundation User APIs use the OAuth 2.0 Authorization Code flow with PKCE for authenticating users and authorizing access to their data. In the current Request Access flow, clients are provisioned as confidential clients by default, so most integrations should perform the code exchange and refresh flow on a server.

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

Use this tutorial when you need to implement OAuth2/OIDC manually, adapt it for a mobile app, support a non-JS stack, or debug token exchange, refresh, scopes, and OpenID Connect validation.

If you want a generated JavaScript app, use Starter With NPX. If you already have a JavaScript app and only need signed-in user API calls, use the User APIs Quickstart with the JavaScript SDK guide. If you only need Quran content without user login, start with the Content APIs Quickstart.

Flow split: Use Client Credentials for Content APIs. Use Authorization Code with PKCE for User APIs.

Prefer a generated Next.js app?

Use Starter With NPX if you want OAuth2, sessions, reader/search, and signed-in user routes already wired. Continue here if you need to implement or debug the OAuth2/OIDC flow manually. The scaffold command is npx @quranjs/create-app@latest my-quran-app --template next --package-manager npm --install --git --sdk-source npm --yes.

Client Types

Confidential vs Public Clients

Quran Foundation issues two types of OAuth2 clients. This distinction affects how you exchange codes and refresh tokens in every step below.

Confidential Client (default)Public Client
Has client_secret?Yes — keep it server-side onlyNo
Token exchangeServer-side with client authenticationIn-app with PKCE only
Refresh tokensServer-sideIn-app or via backend proxy
Typical patternApp handles login + PKCE → sends code to backend → backend exchanges with secretApp exchanges code directly

Most Request Access clients are confidential by default. Your client is public only if Quran Foundation has explicitly confirmed it.

If you call /oauth2/token without client authentication on a confidential client, Hydra returns invalid_client.

After OAuth2 login, validate the ID token according to your OIDC library and use the sub claim as the user's stable Quran Foundation identifier. Store sub as the foreign key in your own database so you can attach your app's custom records to the same user.

ID Token ClaimDescriptionUse For
subUnique user ID (stable, never changes)Primary key for linking to your DB
emailUser's email (when available)Display, notifications
first_nameUser's first namePersonalization
last_nameUser's last namePersonalization, display
Don't use email as primary key

Always use sub as the foreign key to link users. Email addresses can change, but sub is permanent and unique.


Overview

Here's how the OAuth2 Authorization Code flow works when user consent is needed to access their data:

If Quran Foundation explicitly confirms that your client is public, the app can exchange the code directly. For the current Request Access flow, use the backend exchange pattern shown above.

Manual OAuth Implementation Prompt

Use this prompt if you want an AI coding tool to implement the manual OAuth2 flow from this tutorial:

Implement Quran Foundation User APIs authentication with OAuth2 Authorization Code + PKCE and OpenID Connect.

Follow these requirements:
- Request access first and keep QF_CLIENT_SECRET server-side for confidential clients.
- Build the authorization URL with state, nonce, code_challenge, and code_challenge_method=S256.
- Validate state on callback, then exchange code + redirect_uri + code_verifier at POST {authBaseUrl}/oauth2/token.
- For confidential clients, exchange and refresh tokens on the backend with client authentication.
- Store refresh tokens securely and refresh access tokens once when they expire.
- Call User APIs through the backend or proxy with x-auth-token and x-client-id.
- Never log authorization codes, code_verifier, access_token, refresh_token, id_token, or client_secret.

Use these docs:
- OAuth2 tutorial: https://api-docs.quran.foundation/docs/tutorials/oidc/getting-started-with-oauth2/
- User APIs quickstart: https://api-docs.quran.foundation/docs/tutorials/oidc/user-apis-quickstart/
- OpenID Connect: https://api-docs.quran.foundation/docs/tutorials/oidc/openid-connect/

Step 1: Request OAuth 2.0 Client Credentials

  1. Submit an Application to obtain your OAuth client credentials: you will always receive a client_id, and confidential clients will also receive a client_secret for server-side use only.
  2. Provide one or more exact redirect_uri values. These must match exactly at runtime.

Which client type do I have? Most Request Access clients are confidential — let the app handle login + PKCE, then exchange and refresh tokens on your backend. Never embed client_secret in a browser or mobile app.


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_access 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_access%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.

Use the exact scopes approved for your client. If Quran Foundation approved a parent scope such as note, post, or collection, request that parent scope in the authorization URL. Do not also request child scopes such as note.read or post.create unless those child scopes were separately approved for the client. See OAuth2 Scopes.

Tip: Always persist your code_verifier before redirecting to /oauth2/auth. For current Request Access clients, keep it either in app state until callback and send it to your backend exchange endpoint, or keep it in a server-side session if the backend initiates the login flow. Public native/mobile clients should only redeem the code directly if Quran Foundation explicitly confirms that the client is public.

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

Authorization URL builder (Node, server-side):

const crypto = require("crypto");
const { getQfOAuthConfig } = require("./qfOAuthConfig"); // your Step 1 config
const { generatePkcePair } = require("./pkce"); // from PKCE helpers above

function randomString(bytes = 16) {
return crypto.randomBytes(bytes).toString("hex");
}

/**
* Store { state, nonce, codeVerifier, redirectUri } in a server session
* BEFORE redirecting, then validate state on callback and use codeVerifier
* in the token exchange.
*/
function buildAuthorizationUrl({
redirectUri,
scope = "openid offline_access user collection",
}) {
const { authBaseUrl, clientId } = getQfOAuthConfig();
const { codeVerifier, codeChallenge } = generatePkcePair();

const state = randomString(16);
const nonce = randomString(16);

const params = new URLSearchParams();
params.set("response_type", "code");
params.set("client_id", clientId);
params.set("redirect_uri", redirectUri);
params.set("scope", scope);
params.set("state", state);
params.set("nonce", nonce);
params.set("code_challenge", codeChallenge);
params.set("code_challenge_method", "S256");

const url = `${authBaseUrl}/oauth2/auth?${params.toString()}`;

return {
url,
// persist these server-side (session or secure httpOnly cookie)
pkce: { state, nonce, codeVerifier },
};
}

module.exports = { buildAuthorizationUrl };

Step 3: Exchange Code for Tokens

  • Token endpoint: /oauth2/token
  • Method: POST
  • How you authenticate depends on your client type.
  • If you used PKCE in Step 2, always send code_verifier here.

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

JavaScript (Express backend exchange endpoint for mobile/frontends) — recommended for confidential clients:

const express = require("express");
const axios = require("axios");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

const authBaseUrl = "https://prelive-oauth2.quran.foundation";

app.post("/api/auth/qf/exchange", async (req, res) => {
const { code, codeVerifier, redirectUri } = req.body;

try {
const params = new URLSearchParams();
params.append("grant_type", "authorization_code");
params.append("code", code);
params.append("redirect_uri", redirectUri);
params.append("code_verifier", codeVerifier);

const tokenResponse = await axios.post(
`${authBaseUrl}/oauth2/token`,
params.toString(),
{
headers: { "Content-Type": "application/x-www-form-urlencoded" },
auth: {
username: process.env.QF_CLIENT_ID,
password: process.env.QF_CLIENT_SECRET,
},
}
);

const token = tokenResponse.data;
const user = token.id_token ? jwt.decode(token.id_token) : null;

res.json({
accessToken: token.access_token,
refreshToken: token.refresh_token,
idToken: token.id_token,
expiresIn: token.expires_in,
user,
});
} catch (error) {
res.status(500).json({ error: "Failed to exchange authorization code" });
}
});

This is the recommended pattern for React Native, iOS, Android, or browser apps using confidential clients issued through Request Access.

cURL (confidential server 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 client)
const axios = require("axios");
const { getQfOAuthConfig } = require("./qfOAuthConfig");

async function exchangeCodeForTokens({ code, redirectUri, codeVerifier }) {
// Only use this variant if Quran Foundation provisioned the OAuth client as public.
// Otherwise Hydra will reject the token request with invalid_client.
const { authBaseUrl, clientId } = getQfOAuthConfig();

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(
`${authBaseUrl}/oauth2/token`,
params.toString(),
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
);

return res.data;
}

module.exports = { exchangeCodeForTokens };
JavaScript (Node, axios — confidential server client)
const axios = require("axios");
const { getQfOAuthConfig } = require("./qfOAuthConfig");

async function exchangeCodeForTokensConfidential({
code,
redirectUri,
codeVerifier,
}) {
// Confidential clients keep the client secret on the server and
// perform the code exchange on the server.
const { authBaseUrl, clientId, clientSecret } = getQfOAuthConfig();

if (!clientSecret) {
throw new Error(
"Client secret is required for confidential client token exchange"
);
}

const params = new URLSearchParams();
params.append("grant_type", "authorization_code");
params.append("code", code);
params.append("redirect_uri", redirectUri);
params.append("code_verifier", codeVerifier);

const res = await axios.post(
`${authBaseUrl}/oauth2/token`,
params.toString(),
{
headers: { "Content-Type": "application/x-www-form-urlencoded" },
auth: { username: clientId, password: clientSecret },
}
);

return res.data;
}

module.exports = { exchangeCodeForTokensConfidential };
Python (requests — public PKCE client)
import requests
from qf_oauth_config import get_qf_oauth_config # your Step 1 config

def exchange_code_for_tokens(code: str, redirect_uri: str, code_verifier: str):
cfg = get_qf_oauth_config()

# Only use this variant if Quran Foundation provisioned the OAuth client as public.
# Otherwise Hydra will reject the token request with invalid_client.
resp = requests.post(
f"{cfg['authBaseUrl']}/oauth2/token",
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={
'grant_type': 'authorization_code',
'client_id': cfg['clientId'],
'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_access 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

Note: the collections examples below require the collection scope, and other User API endpoints may require additional scopes such as user. A token granted only openid offline_access is sufficient for login and refresh, but not for every User API call.

For confidential web clients, the recommended pattern is to have your backend or serverless proxy send these headers on outbound requests to Quran Foundation. Your app can keep the user session in a server session or secure httpOnly cookies while the backend injects x-auth-token and x-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");
const { getQfOAuthConfig } = require("./qfOAuthConfig");

async function getCollections({ accessToken }) {
const { apiBaseUrl, clientId } = getQfOAuthConfig();

const res = await axios.get(`${apiBaseUrl}/auth/v1/collections?first=1`, {
headers: { "x-auth-token": accessToken, "x-client-id": clientId },
});

return res.data;
}

module.exports = { getCollections };
Python
import requests
from qf_oauth_config import get_qf_oauth_config # your Step 1 config

def get_collections(access_token: str):
cfg = get_qf_oauth_config()
r = requests.get(
f"{cfg['apiBaseUrl']}/auth/v1/collections?first=1",
headers={'x-auth-token': access_token, 'x-client-id': cfg['clientId']}
)
return r.json()

Step 5: Refresh the Access Token (offline_access scope)

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

The same client type rules apply during refresh. For confidential clients (the default), refresh on your backend server.

cURL (confidential server 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=refresh_token&refresh_token=YOUR_REFRESH_TOKEN'
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=refresh_token&client_id=YOUR_CLIENT_ID&refresh_token=YOUR_REFRESH_TOKEN'

JavaScript (Node):

const axios = require("axios");
const { getQfOAuthConfig } = require("./qfOAuthConfig");

async function refreshAccessToken({ refreshToken }) {
const { authBaseUrl, clientId, clientSecret } = getQfOAuthConfig();

const params = new URLSearchParams();
params.append("grant_type", "refresh_token");
params.append("refresh_token", refreshToken);

// Confidential server apps should use HTTP Basic and keep refresh on the server.
// Public PKCE apps should only omit client_secret if Quran Foundation
// explicitly confirmed that the client is public.
const isConfidential = Boolean(clientSecret);

if (!isConfidential) {
params.append("client_id", clientId);
}

const res = await axios.post(
`${authBaseUrl}/oauth2/token`,
params.toString(),
{
headers: { "Content-Type": "application/x-www-form-urlencoded" },
...(isConfidential
? { auth: { username: clientId, password: clientSecret } }
: {}),
}
);

return res.data;
}

module.exports = { refreshAccessToken };
Python
import requests
from qf_oauth_config import get_qf_oauth_config # your Step 1 config

def refresh_access_token(refresh_token: str):
cfg = get_qf_oauth_config()

data = {'grant_type': 'refresh_token', 'refresh_token': refresh_token}

# Confidential server apps use HTTP Basic and keep refresh on the server.
# Public clients only omit client_secret if Quran Foundation explicitly
# confirmed that the client is public.
if cfg.get('clientSecret'):
resp = requests.post(
f"{cfg['authBaseUrl']}/oauth2/token",
auth=(cfg['clientId'], cfg['clientSecret']),
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=data
)
else:
data['client_id'] = cfg['clientId']
resp = requests.post(
f"{cfg['authBaseUrl']}/oauth2/token",
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=data
)

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.
  • Provisioning mode: See Confidential vs Public Clients — the default is confidential.
  • Scopes: Use openid offline_access user collection. Add more scopes only as needed.
  • Parent scopes: If your client is approved for a parent scope like note, request note in the authorization URL; do not request note.read, note.create, or other child scopes unless they were approved separately.
  • Scopes note: Quran.Foundation expects offline_access (not offline).
  • Token storage: Store refresh_token securely; rotate if compromised.
  • Browser-origin policy: Direct browser calls to User APIs require the calling origin to be allowlisted by the target service. If your origin is not allowlisted, call the API from your backend instead.
  • 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 type, using the public-client flow with a confidential client, missing secret for a confidential client, or wrong environmentVerify whether the client is public or confidential, confirm client_id/secret, and confirm the environment. If the client came from Request Access, switch to the server-side exchange unless QF explicitly confirmed the client is public
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, not approved for the client, or a child scope was requested when only the parent scope was approvedRequest the exact approved scopes. If approved for note, request note, not note.read or note.create unless those child scopes were separately approved
401 Unauthorized (APIs)Missing/expired tokenSend x-auth-token and x-client-id, refresh token
403 Forbidden (APIs)Direct browser call from a restricted origin, or scope/permission is not grantedFor web apps, route the request through your backend or proxy; otherwise use a supported browser origin and request the correct scope

Frequently Asked Questions

What scopes should I request for Quran Foundation OAuth2?

Use openid offline_access for authentication-only flows. If your app will call User APIs like the collections examples in this guide, request the corresponding API scopes as well, such as user and collection. Follow the principle of least privilege - only request additional scopes for things like user profile, bookmarks, collections, and similar features when your app actually needs them.

Request the exact scopes approved for your client. If your client is approved for a parent scope such as note, request note; do not request note.read, note.create, or other child scopes unless they were separately approved. See OAuth2 Scopes.

How long do Quran Foundation access tokens last?

Access tokens expire after 1 hour (3,600 seconds). Use the refresh_token (granted via the offline_access scope) to obtain a new access token without requiring the user to log in again. See Step 5: Refresh the Access Token for implementation details.

Can I use the same tokens across pre-production and production?

No. Tokens issued in the pre-production environment (prelive-oauth2.quran.foundation) will not work against the production API (apis.quran.foundation), and vice versa. Always ensure your authBaseUrl and apiBaseUrl point to the same environment.

EnvironmentAuth URLAPI Base URL
Pre-Productionhttps://prelive-oauth2.quran.foundationhttps://apis-prelive.quran.foundation
Productionhttps://oauth2.quran.foundationhttps://apis.quran.foundation

What is the difference between public and confidential OAuth2 clients?

Confidential clients have a client_secret that is stored securely on a backend server and used during token exchange and refresh. This is the default for clients issued through Request Access.

Public clients (SPAs, mobile apps) rely solely on PKCE for security and never use a client_secret. Your client is public only if Quran Foundation has explicitly confirmed it.

If you're unsure, assume your client is confidential and use the backend exchange pattern described in Step 3.

What headers do I need to call Quran Foundation User APIs?

Every request to User APIs requires two custom headers:

x-auth-token: YOUR_ACCESS_TOKEN
x-client-id: YOUR_CLIENT_ID

For web apps, your backend or serverless proxy should usually be the component that sends these headers to Quran Foundation. See Step 4: Call User APIs with Headers for complete examples in cURL, JavaScript, and Python.

I'm getting invalid_client — what's wrong?

This usually means you're using some variant of the public client token exchange flow (sending client_id in the request body without a secret) when your client was actually confidential. For confidential clients, the token exchange must be authenticated on the server with the client secret. Check the Troubleshooting table for more details.

After a successful OAuth2 login, decode the id_token to get the sub claim — this is the user's unique, permanent identifier. Use sub as a foreign key in your own database to associate your app's custom data (premium status, preferences, analytics) with the Quran Foundation user. See How to Link QF Users to Your Data for code examples.