| sidebar_position | 4 |
|---|---|
| displayed_sidebar | APIsSidebar |
This guide walks you through our official OAuth2 Web Example — a production-ready Node.js + Express app with session management, JWT decoding, and proper logout handling.
:::tip Live Demo Try the hosted demo at oauth2-client-example.quran.foundation :::
Clone and run in under 2 minutes:
git clone https://github.com/quran/quran-oauth2-client-example.git
cd quran-oauth2-client-example
cp .env.example .env
npm install
npm startEdit .env with your credentials:
PORT=3000
BASE_PATH=http://localhost:3000
TOKEN_HOST=https://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:::info Get Your Credentials
The example requires valid OAuth2 credentials. Request access here and register http://localhost:3000/callback as your redirect URI.
:::
- Go to http://localhost:3000
- Click "Continue with Quran.Foundation"
- Log in (or create an account) on Quran.com
- Consent to the requested permissions
- You'll be redirected back with your user profile displayed!
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 |
Use the access_token to call User APIs:
const response = await fetch(
"https://apis.quran.foundation/auth/v1/bookmarks",
{
headers: {
"x-auth-token": access_token,
"x-client-id": process.env.CLIENT_ID,
},
}
);
const bookmarks = await response.json();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
| Dependency | Description |
|---|---|
| Express | Minimal Node.js web framework |
| simple-oauth2 | OAuth2 client library |
| express-session | Session management |
| jsonwebtoken | JWT decoding for user info |
| Pug | Templating engine |
| 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 |
const { AuthorizationCode } = require("simple-oauth2");
const client = new AuthorizationCode({
client: {
id: process.env.CLIENT_ID,
secret: process.env.CLIENT_SECRET,
},
auth: {
tokenHost: process.env.TOKEN_HOST,
tokenPath: "/oauth2/token",
authorizePath: "/oauth2/auth",
},
});const crypto = require("crypto");
app.get("/auth", (req, res) => {
// Generate cryptographically random state for CSRF protection
const state = crypto.randomBytes(32).toString("hex");
req.session.oauthState = state;
const authorizationUri = client.authorizeURL({
redirect_uri: "http://localhost:3000/callback",
scope: process.env.SCOPES,
state,
});
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)
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
// Validate state parameter to prevent CSRF attacks
if (!state || state !== req.session.oauthState) {
console.error("State mismatch - possible CSRF attack");
return res.status(403).send("Invalid state parameter");
}
// Clear the stored state after validation
delete req.session.oauthState;
try {
const tokenResponse = await client.getToken({
code,
redirect_uri: "http://localhost:3000/callback",
});
// Store in session
req.session.token = tokenResponse.token;
// Decode ID token for user info
const userInfo = jwt.decode(tokenResponse.token.id_token);
req.session.user = userInfo;
res.redirect("/");
} catch (error) {
console.error("Token exchange failed:", error);
res.status(500).send("Authentication failed");
}
});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) => {
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: req.session.token?.id_token,
});
const logoutUrl = `${process.env.TOKEN_HOST}/oauth2/sessions/logout?${params}`;
res.redirect(logoutUrl);
});
});:::warning Pre-Register Your Redirect URI
The post_logout_redirect_uri must be pre-registered in your OAuth2 client's post_logout_redirect_uris configuration. Contact us to update your client settings.
:::
The example is ready for production deployment. It includes:
- ✅ Secure session configuration with
httpOnlycookies - ✅ Proper proxy trust for reverse proxies (Fly.io, etc.)
- ✅ Environment-based cookie security (
secure: truein production) - ✅ Logout from both local session and OAuth2 provider
fly launch
fly secrets set CLIENT_ID=xxx CLIENT_SECRET=xxx SESSION_SECRET=xxx
fly deploy- Full OAuth2 Guide — Detailed walkthrough with AI prompts
- Mobile Apps — iOS, Android, React Native guides
- User APIs Reference — All available endpoints