Skip to main content

Node.js (Express + TypeScript) Integration Guide

This guide demonstrates an enterprise-level setup for integrating Mubarokah ID OAuth 2.0 into a Node.js application using Express and TypeScript. It includes type definitions, a dedicated OAuth service, authentication middleware, and example routes.

Prerequisites

  • Node.js and npm/yarn installed.
  • TypeScript configured in your project.
  • Express framework.
  • An HTTP client like axios: npm install axios or yarn add axios
  • Session management (e.g., express-session): npm install express-session
  • Optional: A library for PKCE generation if not implementing manually (e.g., pkce-challenge).

Project Structure (Conceptual)

Type Definitions

Define interfaces for configuration, token responses, and user information. src/types/mubarokah.ts:
export interface MubarokahConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  baseUrl: string; // e.g., https://accounts.mubarokah.com
}

export interface TokenResponse {
  token_type: 'Bearer';
  expires_in: number;        // Lifetime of access_token in seconds
  access_token: string;
  refresh_token?: string;   // May not be present for all grant types
  scope?: string;           // Granted scopes
}

export interface UserInfo {
  id: number | string; // Can be number or string based on Mubarokah ID's implementation
  name: string;
  email: string;
  profile_picture?: string;
  username: string;
  gender?: string;
}

export interface DetailedUserInfo extends UserInfo {
  bio?: string;
  phone_number?: string;
  place_of_birth?: string;
  date_of_birth?: string; // Typically YYYY-MM-DD
  address?: string;
}

// For storing tokens, potentially with user info
export interface StoredTokenData extends TokenResponse {
  userId: string;
  userInfo?: UserInfo; // Store basic info with token for convenience
  issued_at: number; // Timestamp when token was issued/refreshed
}

Mubarokah OAuth Service

This service encapsulates all interactions with Mubarokah ID’s OAuth endpoints. src/services/MubarokahOAuthService.ts:
import axios, { AxiosInstance, AxiosError } from 'axios';
import crypto from 'crypto'; // For PKCE and state generation
import { MubarokahConfig, TokenResponse, UserInfo, DetailedUserInfo } from '../types/mubarokah';

export class MubarokahOAuthService {
  private httpClient: AxiosInstance;
  private config: MubarokahConfig;

  constructor(config: MubarokahConfig) {
    this.config = config;
    this.httpClient = axios.create({
      baseURL: config.baseUrl,
      timeout: 15000, // 15 seconds timeout
    });

    this.httpClient.interceptors.response.use(
      response => response,
      (error: AxiosError) => {
        if (error.response && error.response.data) {
          const errorData = error.response.data as any;
          const errorMessage = errorData.error_description || errorData.message || errorData.error || 'OAuth API Error';
          // You might want to throw a custom error class here
          return Promise.reject(new Error(errorMessage));
        }
        return Promise.reject(error);
      }
    );
  }

  private generatePKCE(): { verifier: string; challenge: string } {
    const verifier = crypto.randomBytes(32).toString('base64url'); // RFC 7636 recommends 43-128 chars
    const challenge = crypto
      .createHash('sha256')
      .update(verifier)
      .digest('base64url');
    return { verifier, challenge };
  }

  public generateAuthUrl(
    scopes: string[] = ['view-user'],
    usePKCE: boolean = true
  ): { url: string; state: string; codeVerifier?: string } {
    const state = crypto.randomBytes(16).toString('hex');
    let codeVerifier: string | undefined;

    const params = new URLSearchParams({
      response_type: 'code',
      client_id: this.config.clientId,
      redirect_uri: this.config.redirectUri,
      scope: scopes.join(' '),
      state,
    });

    if (usePKCE) {
      const pkce = this.generatePKCE();
      codeVerifier = pkce.verifier;
      params.append('code_challenge', pkce.challenge);
      params.append('code_challenge_method', 'S256');
    }

    return {
      url: `${this.config.baseUrl}/oauth/authorize?${params.toString()}`,
      state,
      codeVerifier,
    };
  }

  public async exchangeCodeForTokens(
    code: string,
    codeVerifier?: string // Required if PKCE was used
  ): Promise<TokenResponse> {
    const params = new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: this.config.clientId,
      client_secret: this.config.clientSecret,
      redirect_uri: this.config.redirectUri,
      code,
    });

    if (codeVerifier) {
      params.append('code_verifier', codeVerifier);
    }

    const response = await this.httpClient.post<TokenResponse>(
      '/oauth/token',
      params,
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    );
    return response.data;
  }

  public async refreshToken(refreshTokenValue: string): Promise<TokenResponse> {
    const params = new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshTokenValue,
      client_id: this.config.clientId,
      client_secret: this.config.clientSecret,
    });

    const response = await this.httpClient.post<TokenResponse>(
      '/oauth/token',
      params,
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    );
    return response.data;
  }

  public async getUserInfo(accessToken: string): Promise<UserInfo> {
    const response = await this.httpClient.get<UserInfo>('/api/user', {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
    });
    return response.data;
  }

  public async getDetailedUserInfo(accessToken: string): Promise<DetailedUserInfo> {
    const response = await this.httpClient.get<DetailedUserInfo>('/api/user/details', {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
    });
    return response.data;
  }

  // Optional: If Mubarokah ID provides an SSO logout endpoint
  public async ssoLogout(accessToken: string): Promise<void> {
    try {
      await this.httpClient.post('/api/logout-sso', {}, { // Or GET, depending on endpoint
        headers: { Authorization: `Bearer ${accessToken}` },
      });
    } catch (error) {
      console.warn('SSO logout request failed. This might be expected if no such endpoint exists or token is already invalid.', error);
    }
  }
}

Token Service (Conceptual)

This service would handle secure storage and retrieval of tokens. For brevity, a full implementation (e.g., using Redis or a database) is omitted. src/services/TokenService.ts (Interface and basic in-memory example):
import { StoredTokenData, UserInfo, TokenResponse } from "../types/mubarokah";

// This should be a robust storage like Redis or a database in production
const tokenStore: Map<string, StoredTokenData> = new Map();

export class TokenService {
  // In a real app, these would interact with a persistent store
  public async storeTokens(userId: string, tokens: TokenResponse, userInfo?: UserInfo): Promise<void> {
    const storedData: StoredTokenData = {
      ...tokens,
      userId,
      userInfo,
      issued_at: Math.floor(Date.now() / 1000),
    };
    tokenStore.set(userId, storedData); // Replace with DB/Redis write
    console.log(`Tokens stored for user ${userId}`);
  }

  public async getStoredTokens(userId: string): Promise<StoredTokenData | null> {
    const data = tokenStore.get(userId); // Replace with DB/Redis read
    if (!data) return null;

    // Check if access token is expired (with a 5-minute buffer)
    const expiresIn = data.expires_in || 3600; // Default to 1 hour if not present
    const isExpired = data.issued_at + expiresIn - 300 < Math.floor(Date.now() / 1000);

    if (isExpired && data.refresh_token) {
      // Conceptual: Implement token refresh logic here or in MubarokahOAuthService
      console.log(`Token for user ${userId} expired, needs refresh.`);
      // Potentially throw an error or return a flag to trigger refresh
    }
    return data;
  }

  public async deleteTokens(userId: string): Promise<void> {
    tokenStore.delete(userId); // Replace with DB/Redis delete
    console.log(`Tokens deleted for user ${userId}`);
  }
}
The TokenService above uses an in-memory store, which is not suitable for production. Use a persistent store like Redis or a database, and encrypt sensitive tokens.

Authentication Middleware

This middleware protects routes that require authentication. src/middleware/auth.ts:
import { Request, Response, NextFunction } from 'express';
import { TokenService } from '../services/TokenService'; // Assuming you have this
import { UserInfo } from '../types/mubarokah';

// Augment Express Request type
export interface AuthenticatedRequest extends Request {
  user?: UserInfo; // User profile from Mubarokah ID
  accessToken?: string;
}

const tokenService = new TokenService(); // Initialize your token service

export const requireAuth = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    // Check session if no Bearer token (for web clients)
    if (req.session && req.session.userId && req.session.accessToken) {
        const storedData = await tokenService.getStoredTokens(req.session.userId);
        // Basic check: ensure session token matches stored token and user info exists
        if (storedData && storedData.access_token === req.session.accessToken && storedData.userInfo) {
            req.user = storedData.userInfo;
            req.accessToken = storedData.access_token;
            return next();
        }
    }
    return res.status(401).json({ error: 'Unauthorized: No token provided or session invalid.' });
  }

  const token = authHeader.substring(7); // Remove "Bearer "

  try {
    // This part is for API clients sending Bearer token directly.
    // It needs a robust way to validate the token.
    // Example: Use a cache or lightweight DB check if Mubarokah ID doesn't have an introspection endpoint.
    // For now, we'll try to look it up as if it were a user ID, which is NOT secure for Bearer tokens.
    // YOU MUST IMPLEMENT PROPER TOKEN VALIDATION HERE.
    // This could involve:
    // 1. Calling a Mubarokah ID token introspection endpoint.
    // 2. If tokens are JWTs, validating the signature and claims locally.
    // 3. Looking up the token in your secure token store if you store all active tokens.

    // Placeholder: The following is NOT a secure way to validate bearer tokens.
    // It assumes the token itself might be a key to find user info, which is unlikely.
    // Replace with actual token validation logic.
    // const validatedUser = await validateBearerToken(token); // Implement this function
    // if (validatedUser) {
    //   req.user = validatedUser;
    //   req.accessToken = token;
    //   return next();
    // }

    // If relying on session for browser clients, and Bearer for API clients:
    // The session check above handles browser clients.
    // For API clients, you need a different validation mechanism for the Bearer token.
    // The current example is more suited for session-based auth after initial OAuth.

    // If no specific Bearer token validation is implemented, deny access for Bearer tokens.
    console.warn("Bearer token received but no specific validation logic implemented for it in requireAuth.");
    return res.status(401).json({ error: 'Unauthorized: Invalid token or validation method not implemented for Bearer token.' });

  } catch (error) {
    console.error('Authentication error:', error);
    return res.status(401).json({ error: 'Unauthorized: Token validation failed.' });
  }
};
The requireAuth middleware is simplified. Production systems often use JWTs or session management. If using JWTs, validate the signature and claims. If session-based, ensure the session is valid. The provided example is more geared towards session-based authentication after the OAuth dance, with a placeholder for separate Bearer token validation for API clients.

Auth Routes

Define routes for login, callback, and logout. src/routes/auth.ts:
import { Router, Request, Response } from 'express';
import { MubarokahOAuthService } from '../services/MubarokahOAuthService';
import { TokenService } from '../services/TokenService';
import { MubarokahConfig } from '../types/mubarokah';
import { AuthenticatedRequest, requireAuth } from '../middleware/auth'; // Assuming session middleware is set up in app.ts

// Load config (ensure these are set in your environment variables)
const mubarokahConfig: MubarokahConfig = {
  clientId: process.env.MUBAROKAH_CLIENT_ID!,
  clientSecret: process.env.MUBAROKAH_CLIENT_SECRET!,
  redirectUri: process.env.MUBAROKAH_REDIRECT_URI!,
  baseUrl: process.env.MUBAROKAH_BASE_URL || 'https://accounts.mubarokah.com',
};

if (!mubarokahConfig.clientId || !mubarokahConfig.clientSecret || !mubarokahConfig.redirectUri) {
  throw new Error("Mubarokah ID OAuth environment variables are not properly configured.");
}

const oauthService = new MubarokahOAuthService(mubarokahConfig);
const tokenService = new TokenService();
const router = Router();

// Extend Express session data
declare module 'express-session' {
  interface SessionData {
    oauthState?: string;
    codeVerifier?: string;
    userId?: string; // Store Mubarokah User ID in session
    accessToken?: string; // Store access token in session (consider security implications)
  }
}

// Route to start the OAuth login process
router.get('/login/mubarokah', (req: Request, res: Response) => {
  const usePKCE = true; // Recommended
  const authData = oauthService.generateAuthUrl(['view-user', 'detail-user'], usePKCE);

  if (req.session) {
    req.session.oauthState = authData.state;
    if (usePKCE && authData.codeVerifier) {
      req.session.codeVerifier = authData.codeVerifier;
    }
  } else {
    // Handle case where session is not available, though express-session should provide it
    return res.status(500).send("Session not available.");
  }
  res.redirect(authData.url);
});

// OAuth Callback route
router.get('/auth/mubarokah/callback', async (req: Request, res: Response) => {
  const { code, state, error, error_description } = req.query;

  if (error) {
    console.error('OAuth Error:', error, error_description);
    return res.status(400).json({ error, description: error_description || 'OAuth authentication failed.' });
  }

  const sessionState = req.session?.oauthState;
  const codeVerifier = req.session?.codeVerifier;

  if (!sessionState || sessionState !== state) {
    console.error('Invalid OAuth state:', { sessionState, callbackState: state });
    return res.status(400).json({ error: 'invalid_state', description: 'State parameter mismatch. Possible CSRF attack.' });
  }

  // Clear state and verifier from session after use
  if (req.session) {
    delete req.session.oauthState;
    delete req.session.codeVerifier;
  }

  if (!code) {
    return res.status(400).json({ error: 'missing_code', description: 'Authorization code is missing.' });
  }

  try {
    const tokens = await oauthService.exchangeCodeForTokens(code as string, codeVerifier);
    const userInfo = await oauthService.getUserInfo(tokens.access_token);

    // Store tokens and user info (e.g., in your database or a secure session store)
    await tokenService.storeTokens(userInfo.id.toString(), tokens, userInfo);

    // Establish user session for your application
    if (req.session) {
      req.session.userId = userInfo.id.toString();
      req.session.accessToken = tokens.access_token; // Be cautious about storing access tokens in session
                                                     // depending on your security model. HttpOnly cookies are safer.
    }

    // Redirect to a logged-in area, e.g., dashboard
    res.redirect('/dashboard'); // Or return JSON for API clients

  } catch (err) {
    const castError = err as Error;
    console.error('OAuth Callback Processing Error:', castError);
    res.status(500).json({ error: 'callback_processing_failed', description: castError.message });
  }
});

// Example protected route
router.get('/profile', requireAuth, (req: AuthenticatedRequest, res: Response) => {
  res.json({
    message: "This is a protected profile page.",
    user: req.user, // UserInfo from requireAuth middleware
    accessTokenUsed: req.accessToken // Access token from requireAuth middleware
  });
});


// Logout route
router.post('/logout', requireAuth, async (req: AuthenticatedRequest, res: Response) => {
  const userId = req.session?.userId;
  const accessToken = req.session?.accessToken || req.accessToken;

  try {
    if (userId) {
      await tokenService.deleteTokens(userId); // Remove tokens from your storage
    }
    if (accessToken) {
      // Optional: Call Mubarokah ID's SSO logout if available and desired
      // await oauthService.ssoLogout(accessToken);
    }

    req.session?.destroy(err => {
      if (err) {
        console.error('Session destruction error:', err);
        return res.status(500).json({ error: 'Logout failed during session destruction.' });
      }
      res.clearCookie('connect.sid'); // Default express-session cookie name, adjust if different
      res.json({ success: true, message: 'Successfully logged out.' });
    });
  } catch (err) {
    const castError = err as Error;
    console.error('Logout error:', castError);
    res.status(500).json({ error: 'Logout failed.', description: castError.message });
  }
});

export default router;

Application Setup

Integrate the Mubarokah OAuth service and routes into your Express application. src/app.ts (Simplified):
import express from 'express';
import session from 'express-session'; // npm install express-session @types/express-session
import authRoutes from './routes/auth';
// import { requireAuth } from './middleware/auth'; // If you have other protected routes

const app = express();
const port = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session middleware - REQUIRED for storing OAuth state and user session
app.use(session({
  secret: process.env.SESSION_SECRET || 'your_very_strong_session_secret', // Change in production!
  resave: false,
  saveUninitialized: false, // Set to true if you want to store session for all visitors
  cookie: {
    secure: process.env.NODE_ENV === 'production', // Use secure cookies in production (HTTPS)
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours, for example
  }
}));

// Routes
app.use('/auth', authRoutes); // Mount auth routes under /auth path

app.get('/', (req, res) => {
  if (req.session?.userId) {
    res.send(\`<h1>Welcome</h1><p>User ID: \${req.session.userId}</p><form action="/auth/logout" method="post"><button type="submit">Logout</button></form>\`);
  } else {
    res.send('<h1>Welcome</h1><p><a href="/auth/login/mubarokah">Login with Mubarokah ID</a></p>');
  }
});

app.get('/dashboard', (req, res) => {
  if (!req.session?.userId) {
    return res.redirect('/');
  }
  res.send(\`<h1>Dashboard</h1><p>User ID: \${req.session.userId}. Your Access Token (from session): \${req.session.accessToken}</p> <p><a href="/auth/profile">View Profile (Protected)</a></p> <form action="/auth/logout" method="post"><button type="submit">Logout</button></form>\`);
});


// Start server in server.ts
// app.listen(port, () => {
//   console.log(\`Server running at http://localhost:\${port}\`);
// });

export default app;
Security Note:
  • Use a strong, unique SESSION_SECRET stored as an environment variable in production.
  • Ensure secure: true for cookies in production (requires HTTPS).
  • The example TokenService is for demonstration. Use a robust, secure storage solution for tokens in production.
  • Storing raw access tokens in the session might have security implications depending on your session store’s security. Consider storing only a session ID and retrieving tokens from a secure backend store when needed.
This setup provides a solid foundation for integrating Mubarokah ID OAuth with Node.js, Express, and TypeScript, including session management and basic security considerations. Remember to adapt and enhance the token storage and session management for your production environment.