nodejsnotes

Topic 023: Building a Token Management System and Handling Conditional Redirects in Node.js: A Step-by-Step Guide to Expiration, Logging, and User Workflow Management

Topic 023: Building a Token Management System and Handling Conditional Redirects in Node.js: A Step-by-Step Guide to Expiration, Logging, and User Workflow Management


Let’s break this down step by step and build a Node.js code that handles tokens, checks expiration, and fetches new tokens when needed. We’ll also incorporate logging via console.log to track the operations.

Breakdown of Concepts

  1. Optional chaining (?.):
    • record?.value means if record exists, return record.value; otherwise, return undefined. This prevents errors when trying to access properties of null or undefined.
  2. Token expiration:
    • Date.now().valueOf() / 1000 gives the current time in seconds (not milliseconds, so / 1000 is used).
    • exp refers to the expiration time of the token, which is a standard field in JWT (JSON Web Token). We compare it with the current time to check if the token is expired.
  3. Decoding JWT:

    • Using jwt.decode(), we extract the payload from the token, which contains the exp field (the token’s expiration time).
    • decodetoken.payload.exp * 1000 multiplies the expiration time (exp) by 1000 to convert it into milliseconds.
  4. Cache expiration logic:
    • We save the token until its expiration and then fetch a new token once it’s expired.

Node.js Code Example with jsonwebtoken (for decoding JWTs)

const jwt = require("jsonwebtoken"); // Install using npm: npm install jsonwebtoken

// Simulated token record from MOD OIDC URL
let record = {
  value: "your.jwt.token.here", // This is a placeholder JWT token
};

// Function to simulate token fetching and caching
function handleToken() {
  const token = record?.value; // Optional chaining to get the token value
  if (!token) {
    console.log("No token found in record.");
    return;
  }

  // Get the current time in seconds
  const currTime = Math.floor(Date.now() / 1000);
  console.log(`Current time (in seconds): ${currTime}`);

  // Decode the token to extract the payload
  let decodedToken;
  try {
    decodedToken = jwt.decode(token, { complete: true }); // Decoding JWT
  } catch (err) {
    console.error("Error decoding token:", err);
    return;
  }

  if (!decodedToken || !decodedToken.payload) {
    console.log("Invalid token payload.");
    return;
  }

  const exp = decodedToken.payload.exp; // Get the expiration time from the payload
  console.log(`Token expiration time (exp, in seconds): ${exp}`);

  // Check if the token has expired
  if (exp > currTime) {
    console.log("Token is still valid.");
  } else {
    console.log("Token has expired. Fetching a new token...");
    // Logic to fetch a new token can go here
    // After fetching, save the new token back to the cache (record.value)
    // Example:
    // record.value = fetchNewToken();
  }

  // Calculate the cache expiration in milliseconds
  const cacheExp = exp * 1000;
  console.log(`Cache expiration time (in milliseconds): ${cacheExp}`);

  // Save the token in the cache until it expires
  setTimeout(() => {
    console.log("Token expired. Fetching a new token...");
    // Fetch new token and store it
    // record.value = fetchNewToken();
  }, cacheExp - Date.now()); // Subtract current time from cache expiration

  console.log("Token will be cached until expiration.");
}

// Example function to fetch a new token (not implemented)
function fetchNewToken() {
  // Simulated token fetch logic
  const newToken = "new.jwt.token.here";
  console.log("New token fetched:", newToken);
  return newToken;
}

// Run the token handler function
handleToken();

Scenario

Imagine you have a web application where users must satisfy certain conditions (password reset, accepting terms, verifying email, etc.) before they can fully access their dashboard. After logging in, the system checks the user’s state and redirects them accordingly.

Conditional Redirection Flow

When the user successfully logs in, we’ll check:

  1. If the password needs to be changed, redirect to /password-change.
  2. If they haven’t accepted the latest legal terms, redirect to /legal.
  3. If the email is not verified, redirect to /verify-email.
  4. If the profile is incomplete, redirect to /complete-profile.
  5. If MFA is not enabled, redirect to /enable-mfa.

Node.js Code Example

Here’s a Node.js app that demonstrates these conditions:

const express = require("express");
const jwt = require("jsonwebtoken"); // For decoding JWT tokens
const app = express();
const PORT = 3000;

// Mock user data (in real scenarios, you'd fetch this from a database)
const user = {
  id: 1,
  emailVerified: false,
  passwordExpired: true,
  termsAccepted: false,
  profileComplete: false,
  mfaEnabled: false,
};

// Middleware to simulate token and user info retrieval
app.get("/auth/callback", (req, res) => {
  const returnTo = req.query.returnto || "/dashboard"; // Default to dashboard if no returnTo provided
  const locale = req.query.loc || "en"; // Default to English if no locale is provided
  const token = req.query.token || "your-jwt-token-here"; // Authorization token from URL

  // Decode the JWT to get the user information (simulated for this example)
  const decodedToken = jwt.decode(token);

  console.log(`Return URL: ${returnTo}`);
  console.log(`Locale: ${locale}`);
  console.log(`Authorization Token: ${token}`);

  // Conditional redirection logic based on user state
  if (user.passwordExpired) {
    console.log("Password is expired. Redirecting to password change page.");
    return res.redirect(`/password-change?loc=${locale}&token=${token}`);
  }

  if (!user.termsAccepted) {
    console.log("Terms & Conditions not accepted. Redirecting to legal page.");
    return res.redirect(`/legal?loc=${locale}&token=${token}`);
  }

  if (!user.emailVerified) {
    console.log("Email not verified. Redirecting to email verification page.");
    return res.redirect(`/verify-email?loc=${locale}&token=${token}`);
  }

  if (!user.profileComplete) {
    console.log("Profile incomplete. Redirecting to profile completion page.");
    return res.redirect(`/complete-profile?loc=${locale}&token=${token}`);
  }

  if (!user.mfaEnabled) {
    console.log("MFA not enabled. Redirecting to enable MFA page.");
    return res.redirect(`/enable-mfa?loc=${locale}&token=${token}`);
  }

  // If all conditions are met, redirect to the returnTo URL
  console.log("All conditions met. Redirecting to:", returnTo);
  res.redirect(`${returnTo}?loc=${locale}&token=${token}`);
});

// Example routes for the different redirection flows
app.get("/password-change", (req, res) => {
  res.send(`<h1>Password Change</h1><p>Please change your password.</p>`);
});

app.get("/legal", (req, res) => {
  res.send(`<h1>Terms & Conditions</h1><p>Please accept the latest terms & conditions.</p>`);
});

app.get("/verify-email", (req, res) => {
  res.send(`<h1>Email Verification</h1><p>Please verify your email address.</p>`);
});

app.get("/complete-profile", (req, res) => {
  res.send(`<h1>Complete Profile</h1><p>Please complete your profile information.</p>`);
});

app.get("/enable-mfa", (req, res) => {
  res.send(`<h1>Enable MFA</h1><p>Please enable Multi-Factor Authentication (MFA).</p>`);
});

// Fallback route for the dashboard
app.get("/dashboard", (req, res) => {
  res.send("<h1>Welcome to your dashboard!</h1>");
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});