Skip to main content

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
Get Your Credentials

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​

  1. Go to http://localhost:3000
  2. Click "Continue with Quran Foundation"
  3. Sign in through the hosted Quran Foundation login
  4. Consent to the requested permissions
  5. You'll be redirected back with your user profile displayed!

What You'll Get​

After successful authentication, you'll receive:

TokenPurpose
access_tokenUse for API calls (valid ~1 hour)
id_tokenContains user profile info (JWT)
refresh_tokenGet new access tokens without re-login
expires_inSeconds 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​

DependencyDescription
ExpressMinimal Node.js web framework
simple-oauth2OAuth2 client library
express-sessionSession management
joseID token signature and claim verification
PugTemplating engine

Key Endpoints​

EndpointDescription
/Home page (shows login or user profile)
/authRedirects to Quran Foundation login
/callbackHandles OAuth2 callback, exchanges code for tokens
/logoutClears 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 scopes
  • state — CSRF protection (validate on callback)
  • nonce — Binds the returned ID token to this login attempt
  • code_challenge — S256 challenge derived from the session-stored code_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);
});
});
Pre-Register Your Redirect URI

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​