Skip to main content

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.

ServiceProduction URLUsed for
APIhttps://api.muhajirstudio.com/Authorization, token exchange, refresh, and /api/external/*.
Main webhttps://login.muhajirstudio.com/User login, signup, email verification, and consent.
Admin webhttps://admin.muhajirstudio.com/Application registration.

Admin Setup

Ask an admin to register your application in web-admin with:

FieldExampleNotes
Allowed web originhttps://app.example.comOrigin only; no path, query, fragment, or wildcard.
Redirect path/auth/callbackPath only.
Redirect URIhttps://app.example.com/auth/callbackDerived from origin + path.
Scopesopenid profile email offline_accessAdmin-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, and code_verifier server-side per login attempt.
  • Validate state before exchanging code.
  • Never send client_secret for 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 401 as re-authentication required and 403 as missing scope or insufficient consent.