Node.js Backend Integration
This recipe shows how a Node.js backend can sign users in with Muhajir Studio, keep OAuth state server-side, and call the external API from trusted server code.
When to Use This Pattern
Use this pattern when your application has a backend that can maintain an HTTP-only application session. The browser only receives your app session cookie; OIDC tokens stay on your server.
| Service | Production URL | Used for |
|---|---|---|
| API | https://api.muhajirstudio.com/ | Authorization, token exchange, refresh, and /api/external/*. |
| Main web | https://login.muhajirstudio.com/ | User login, signup, email verification, and consent. |
| Admin web | https://admin.muhajirstudio.com/ | Application registration. |
Admin Setup
Ask an admin to register your application in web-admin with:
| Field | Example | Notes |
|---|---|---|
| Allowed web origin | https://app.example.com | Origin only; no path, query, fragment, or wildcard. |
| Redirect path | /auth/callback | Path only. |
| Redirect URI | https://app.example.com/auth/callback | Derived from origin + path. |
| Scopes | openid profile email offline_access | Admin-selected scopes are the source of truth. |
Admin-created applications are Public PKCE clients. They receive a client_id and do not receive a client_secret.
1. Start Login
Create a login route in your app that generates PKCE and CSRF values, stores them in the server session, then redirects the browser to Muhajir Studio.
In the examples below, apiBaseUrl is the production API URL and clientId is the public client ID from web-admin.
app.get('/login', async (req, res) => {
const codeVerifier = randomUrlSafeString();
const codeChallenge = await sha256Base64Url(codeVerifier);
const state = randomUrlSafeString();
const nonce = randomUrlSafeString();
req.session.oidc = { codeVerifier, state, nonce };
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: 'https://app.example.com/auth/callback',
response_type: 'code',
scope: 'openid profile email offline_access',
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
res.redirect(`${apiBaseUrl}/api/auth/oauth2/authorize?${params}`);
});
code_challenge_method must be S256. Plain PKCE challenges are not accepted.
2. Handle the Callback
After the user signs in, verifies email if required, and consents, Muhajir Studio redirects to your registered callback with code and state.
Your callback must verify the returned state before token exchange.
app.get('/auth/callback', async (req, res) => {
if (req.query.state !== req.session.oidc?.state) {
res.status(400).send('Invalid state');
return;
}
const tokenRes = await fetch(`${apiBaseUrl}/api/auth/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: String(req.query.code),
code_verifier: req.session.oidc.codeVerifier,
client_id: clientId,
redirect_uri: 'https://app.example.com/auth/callback',
}),
});
const tokenBody = await tokenRes.json();
if (!tokenRes.ok) {
res.status(400).json(tokenBody);
return;
}
// Store tokens server-side, associated with your application session.
req.session.tokens = tokenBody;
delete req.session.oidc;
res.redirect('/');
});
Do not send client_secret for admin-created Public PKCE clients.
3. Call the External API
Use the access token from your server to call stable external endpoints.
const meRes = await fetch(`${apiBaseUrl}/api/external/me`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (meRes.status === 401) {
// Token is missing, expired, unknown, or no longer valid.
}
if (meRes.status === 403) {
// Token is valid but lacks a required scope such as openid.
}
GET /api/external/me always requires openid. Additional fields are returned only when the token includes matching scopes such as profile, email, or permissions.
4. Refresh Tokens
If the token response includes refresh_token, store it encrypted or in a protected server-side session store. Use it only from your backend.
POST /api/auth/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID
If refresh fails with invalid_grant, clear the stored tokens and send the user through login again.
Production Checklist
- Store
state,nonce, andcode_verifierserver-side per login attempt. - Validate
statebefore exchangingcode. - Never send
client_secretfor Public PKCE clients. - Store tokens only on the server, not in browser-accessible storage.
- Register the exact production origin and callback path.
- Handle OAuth callback errors such as
error=access_denied. - Treat
401as re-authentication required and403as missing scope or insufficient consent.