OAuth2 Web Integration Reference
This guide walks through the security-critical parts of a confidential Node.js + Express integration: server sessions, state, nonce, PKCE, verified ID-token claims, and logout. Use the official scaffold below when you want a complete runnable application; treat the standalone snippets as a reference to adapt to your existing Express app.
Setup
For a new SDK-based app, start from the production-style scaffold:
npx @quranjs/create-app@latest my-quran-app --template next --package-manager npm --install --git --sdk-source npm --yes
Use these environment values in your Express app:
PORT=3000
BASE_PATH=http://localhost:3000
TOKEN_HOST=https://prelive-oauth2.quran.foundation
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret
SCOPES=openid offline_access bookmark collection user
SESSION_SECRET=your_random_session_secret
NODE_ENV=development
The Express example performs a secret-backed server exchange. Create a Backend/server app in Developer Console, use its pre-live client_id and one-time client_secret, and register http://localhost:3000/callback as a redirect URI.
The setup below starts in pre-live. Keep the credential, OAuth host, and User API host in the same environment. After production permissions are granted, change both hosts together: use https://oauth2.quran.foundation for OAuth and https://apis.quran.foundation for User APIs.
Expected Flow
- Go to http://localhost:3000
- Click "Continue with Quran Foundation"
- Sign in through the hosted Quran Foundation login
- Consent to the requested permissions
- You'll be redirected back with your user profile displayed!
What You'll Get
After successful authentication, you'll receive:
| Token | Purpose |
|---|---|
access_token | Use for API calls (valid ~1 hour) |
id_token | Contains user profile info (JWT) |
refresh_token | Get new access tokens without re-login |
expires_in | Seconds until access token expires |
Making API Calls
Use the access_token to call User APIs:
const response = await fetch(
"https://apis-prelive.quran.foundation/auth/v1/bookmarks",
{
headers: {
"x-auth-token": access_token,
"x-client-id": process.env.CLIENT_ID,
},
}
);
const bookmarks = await response.json();
This example keeps the user session on the server and uses the server to send x-auth-token and x-client-id to Quran Foundation. That is the recommended pattern for confidential web clients. If you move the resource call into frontend browser code, you leave that server-side pattern and may run into browser-origin restrictions.
Accessing Resources
Once authenticated, explore the User APIs:
- Collections — Create and manage verse collections
- Bookmarks — Save and retrieve bookmarked verses
- Reading Sessions — Track reading progress
- Goals & Streaks — User reading goals
How the Example Works
Dependencies
| Dependency | Description |
|---|---|
| Express | Minimal Node.js web framework |
| simple-oauth2 | OAuth2 client library |
| express-session | Session management |
| jose | ID token signature and claim verification |
| Pug | Templating engine |
Key Endpoints
| Endpoint | Description |
|---|---|
/ | Home page (shows login or user profile) |
/auth | Redirects to Quran Foundation login |
/callback | Handles OAuth2 callback, exchanges code for tokens |
/logout | Clears session and logs out from OAuth2 provider |
Code Walkthrough
1. OAuth2 Client Setup
const { AuthorizationCode } = require("simple-oauth2");
const tokenHost = process.env.TOKEN_HOST.replace(/\/+$/, "");
const issuer = tokenHost;
let jwks;
async function verifyIdToken(idToken) {
const { createRemoteJWKSet, jwtVerify } = await import("jose");
jwks ??= createRemoteJWKSet(
new URL(`${tokenHost}/.well-known/jwks.json`)
);
return jwtVerify(idToken, jwks, {
issuer,
audience: process.env.CLIENT_ID,
});
}
const client = new AuthorizationCode({
client: {
id: process.env.CLIENT_ID,
secret: process.env.CLIENT_SECRET,
},
auth: {
tokenHost,
tokenPath: "/oauth2/token",
authorizePath: "/oauth2/auth",
},
});
2. Authorization URL with State, Nonce, and PKCE
const crypto = require("crypto");
app.get("/auth", (req, res) => {
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
const state = crypto.randomBytes(32).toString("hex");
const nonce = crypto.randomBytes(32).toString("hex");
req.session.oauthRequest = { codeVerifier, nonce, state };
const authorizationUri = client.authorizeURL({
redirect_uri: "http://localhost:3000/callback",
scope: process.env.SCOPES,
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
res.redirect(authorizationUri);
});
Parameters:
redirect_uri— Where to return after login (must match registered URI exactly)scope— Permissions requested. See available scopesstate— CSRF protection (validate on callback)nonce— Binds the returned ID token to this login attemptcode_challenge— S256 challenge derived from the session-storedcode_verifier
3. Token Exchange with PKCE and ID Token Validation
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
const oauthRequest = req.session.oauthRequest;
if (!oauthRequest || !state || state !== oauthRequest.state) {
console.error("State mismatch - possible CSRF attack");
return res.status(403).send("Invalid state parameter");
}
// Make state, nonce, and verifier single-use.
delete req.session.oauthRequest;
try {
const tokenResponse = await client.getToken({
code,
redirect_uri: "http://localhost:3000/callback",
code_verifier: oauthRequest.codeVerifier,
});
const { payload: userInfo } = await verifyIdToken(
tokenResponse.token.id_token
);
if (userInfo.nonce !== oauthRequest.nonce) {
throw new Error("ID token nonce mismatch");
}
req.session.token = tokenResponse.token;
req.session.user = userInfo;
res.redirect("/");
} catch (error) {
console.error("Token exchange failed:", error);
res.status(500).send("Authentication failed");
}
});
4. Logout with Redirect
To redirect users back to your app after logout, include the post_logout_redirect_uri parameter and the id_token_hint from the login response:
app.get("/logout", (req, res) => {
const idTokenHint = req.session.token?.id_token;
if (!idTokenHint) {
return req.session.destroy(() => res.redirect("/"));
}
req.session.destroy(() => {
// Build logout URL with redirect back to app
const params = new URLSearchParams({
post_logout_redirect_uri: `${process.env.BASE_PATH}/`,
id_token_hint: idTokenHint,
});
const logoutUrl = `${process.env.TOKEN_HOST}/oauth2/sessions/logout?${params}`;
res.redirect(logoutUrl);
});
});
The post_logout_redirect_uri must be pre-registered in your OAuth2 client's post_logout_redirect_uris configuration. Open the app in Developer Console and update its redirect URIs for the matching environment.
Production Checklist
Before deploying an implementation based on these snippets:
- Use a durable server-side session store rather than the default in-memory store.
- Set secure,
httpOnly, appropriately scoped cookies and configure proxy trust for your hosting platform. - Keep
client_secret, authorization codes, PKCE verifiers, and all tokens out of logs. - Validate state, ID-token signature and claims, and nonce on every login.
- Register exact production login and post-logout redirect URIs in Developer Console.
Next Steps
- Full OAuth2 Guide — Detailed walkthrough with AI prompts
- Mobile Apps — iOS, Android, React Native guides
- User APIs Reference — All available endpoints