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 axiosoryarn 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:
Copy
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:
Copy
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):
Copy
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:
Copy
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:
Copy
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):
Copy
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_SECRETstored as an environment variable in production. - Ensure
secure: truefor cookies in production (requires HTTPS). - The example
TokenServiceis 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.