User Tools

Site Tools


authentication:send_sms

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
authentication:send_sms [2026/03/09 07:26] – [Start Node.js server:] sonaliauthentication:send_sms [2026/03/09 07:27] (current) – [Upload server.js from local machine (run on Windows):] sonali
Line 327: Line 327:
 </code> </code>
  
-==== Upload server.js from local machine (run on Windows): ====+sample server.js
  
 +<code>## Complete server.js Code
 +javascript
 +
 +const express = require('express');
 +
 +const cors = require('cors');
 +
 +const axios = require('axios');
 +
 +const https = require('https');
 +
 +const qs = require('querystring');
 +
 +const path = require('path');
 +
 +const app = express();
 +
 +app.set('trust proxy', 1);
 +
 +app.use(cors({ origin: '*' }));
 +
 +app.use(express.json());
 +
 +app.use(express.urlencoded({ extended: true }));
 +
 +const httpsAgent = new https.Agent({ rejectUnauthorized: false });
 +
 +const otpSessions = new Map();
 +
 +// Session cleanup (every 5 min, TTL 10 min)
 +
 +setInterval(() => {
 +
 +  const cutoff = Date.now() - 10 * 60 * 1000;
 +
 +  for (const [id, session] of otpSessions) {
 +
 +    if (session.createdAt <cutoff) otpSessions.delete(id);
 +
 +  }
 +
 +}, 5 * 60 * 1000);
 +
 +// ── Helpers ───────────────────────────────────────────────────────────────────
 +
 +async function exchangeCodeForTokens(kcUrl, realm, clientId, code, redirectUri) {
 +
 +  const tokenUrl = `${kcUrl}/realms/${realm}/protocol/openid-connect/token`;
 +
 +  const res = await axios.post(
 +
 +    tokenUrl,
 +
 +    qs.stringify({ grant_type: 'authorization_code', client_id: clientId, code, redirect_uri: redirectUri }),
 +
 +    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, httpsAgent }
 +
 +  );
 +
 +  return res.data;
 +
 +}
 +
 +function parseCookies(setCookieHeader) {
 +
 +  if (!setCookieHeader) return '';
 +
 +  const arr = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
 +
 +  return arr.map(c => c.split(';')[0]).join('; ');
 +
 +}
 +
 +function mergeCookies(existing, incoming) {
 +
 +  const parts = [...new Set([
 +
 +    ...existing.split('; ').filter(Boolean),
 +
 +    ...incoming.split('; ').filter(Boolean),
 +
 +  ])];
 +
 +  return parts.join('; ');
 +
 +}
 +
 +function generateSessionId() {
 +
 +  return Math.random().toString(36).slice(2) + Date.now().toString(36);
 +
 +}
 +
 +// ── SMS OTP Login ─────────────────────────────────────────────────────────────
 +
 +app.post('/api/login-sms', async (req, res) => {
 +
 +  const { kcUrl, realm, phone } = req.body;
 +
 +  const smsClientId = 'sms-login-client';
 +
 +  if (!kcUrl || !realm || !phone)
 +
 +    return res.status(400).json({ error: 'kcUrl, realm, and phone are required.' });
 +
 +  // Step 1: Find username by phone via Keycloak Admin API
 +
 +  let username;
 +
 +  try {
 +
 +    const tokenRes = await axios.post(
 +
 +   `${kcUrl}/realms/master/protocol/openid-connect/token`,
 +
 +      qs.stringify({ grant_type: 'password', client_id: 'admin-cli', username: 'super.admin', password: 'SuperAdmin@26' }),
 +
 +      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, httpsAgent }
 +
 +    );
 +
 +    const adminToken = tokenRes.data.access_token;
 +
 +    const normalizedPhone = phone.startsWith('+91') ? phone.slice(3) : phone.replace(/^0/, '');
 +
 +    const usersRes = await axios.get(`${kcUrl}/admin/realms/${realm}/users`, {
 +
 +      params: { q: `phone:${normalizedPhone}`, max: 5 },
 +
 +      headers: { Authorization: `Bearer ${adminToken}` }, httpsAgent,
 +
 +    });
 +
 +    const match = usersRes.data.find(u => {
 +
 +      const p = (u.attributes?.phone?.[0] || '').replace(/^\+91/, '').replace(/^0/, '');
 +
 +      return p === normalizedPhone;
 +
 +    });
 +
 +    if (!match) return res.status(404).json({ error: 'No user found with this phone number.' });
 +
 +    username = match.username;
 +
 +  } catch (err) {
 +
 +    return res.status(500).json({ error: `Failed to look up user: ${err.response?.data?.error_description || err.message}` });
 +
 +  }
 +
 +  // Step 2: Trigger Keycloak SMS OTP flow
 +
 +  const redirectUri = `${req.protocol}://${req.get('host')}/callback`;
 +
 +  const authUrl = `${kcUrl}/realms/${realm}/protocol/openid-connect/auth`;
 +
 +  try {
 +
 +    const authRes = await axios.get(authUrl, {
 +
 +      params: { client_id: smsClientId, response_type: 'code', scope: 'openid', redirect_uri: redirectUri },
 +
 +      httpsAgent, maxRedirects: 5, validateStatus: s => s <500,
 +
 +    });
 +
 +    if (authRes.status !== 200)
 +
 +      return res.status(400).json({ error: `Keycloak returned HTTP ${authRes.status} — is sms-login-client created?` });
 +
 +    const actionMatch = authRes.data.match(/action="([^"]+)"/);
 +
 +    if (!actionMatch) return res.status(500).json({ error: 'Could not find login form.' });
 +
 +    const loginActionUrl = actionMatch[1].replace(/&/g, '&');
 +
 +    const loginCookies = parseCookies(authRes.headers['set-cookie']);
 +
 +    const credRes = await axios.post(loginActionUrl, qs.stringify({ username }), {
 +
 +      headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': loginCookies },
 +
 +      httpsAgent, maxRedirects: 0, validateStatus: s => s <500,
 +
 +    });
 +
 +    if (credRes.status === 302 && credRes.headers.location?.includes('code=')) {
 +
 +      const code = new URL(credRes.headers.location).searchParams.get('code');
 +
 +      const tokens = await exchangeCodeForTokens(kcUrl, realm, smsClientId, code, redirectUri);
 +
 +      return res.json({ success: true, tokens });
 +
 +    }
 +
 +    let otpHtml = typeof credRes.data === 'string' ? credRes.data : JSON.stringify(credRes.data);
 +
 +    let otpCookies = mergeCookies(loginCookies, parseCookies(credRes.headers['set-cookie']));
 +
 +    if (credRes.status === 302 && credRes.headers.location && !credRes.headers.location.includes('code=')) {
 +
 +      const otpPageRes = await axios.get(credRes.headers.location, {
 +
 +        headers: { 'Cookie': otpCookies }, httpsAgent, maxRedirects: 5, validateStatus: s => s <500,
 +
 +      });
 +
 +      otpHtml = typeof otpPageRes.data === 'string' ? otpPageRes.data : JSON.stringify(otpPageRes.data);
 +
 +      otpCookies = mergeCookies(otpCookies, parseCookies(otpPageRes.headers['set-cookie']));
 +
 +    }
 +
 +    const otpActionMatch = otpHtml?.match(/action="([^"]+)"/);
 +
 +    if (!otpActionMatch)
 +
 +      return res.status(500).json({ error: 'SMS OTP form not found. Check sms-login-client flow and cotrav theme.' });
 +
 +    const sessionId = generateSessionId();
 +
 +    otpSessions.set(sessionId, {
 +
 +      otpActionUrl: otpActionMatch[1].replace(/&/g, '&'),
 +
 +      cookies: otpCookies, kcUrl, realm, clientId: smsClientId, redirectUri, createdAt: Date.now(),
 +
 +    });
 +
 +    return res.json({ requiresOTP: true, sessionId });
 +
 +  } catch (err) {
 +
 +    return res.status(500).json({ error: `SMS login failed: ${err.response?.data ? JSON.stringify(err.response.data) : err.message}` });
 +
 +  }
 +
 +});
 +
 +// ── SMS OTP Verify ────────────────────────────────────────────────────────────
 +
 +app.post('/api/verify-sms-otp', async (req, res) => {
 +
 +  const { sessionId, otp } = req.body;
 +
 +  if (!sessionId || !otp) return res.status(400).json({ error: 'sessionId and otp are required.' });
 +
 +  const session = otpSessions.get(sessionId);
 +
 +  if (!session) return res.status(400).json({ error: 'Session not found or expired.' });
 +
 +  try {
 +
 +    const otpRes = await axios.post(session.otpActionUrl, qs.stringify({ sms_otp: otp }), {
 +
 +      headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': session.cookies },
 +
 +      httpsAgent, maxRedirects: 0, validateStatus: s => s <500,
 +
 +    });
 +
 +    if (otpRes.status === 302 && otpRes.headers.location?.includes('code=')) {
 +
 +      const code = new URL(otpRes.headers.location).searchParams.get('code');
 +
 +      const { kcUrl, realm, clientId, redirectUri } = session;
 +
 +      const tokens = await exchangeCodeForTokens(kcUrl, realm, clientId, code, redirectUri);
 +
 +      otpSessions.delete(sessionId);
 +
 +      return res.json({ success: true, tokens });
 +
 +    }
 +
 +    if (otpRes.status === 200) return res.status(401).json({ error: 'Invalid OTP. Please try again.' });
 +
 +    return res.status(400).json({ error: `Unexpected response: HTTP ${otpRes.status}` });
 +
 +  } catch (err) {
 +
 +    return res.status(500).json({ error: `SMS OTP verification failed: ${err.message}` });
 +
 +  }
 +
 +});
 +
 +// ── Webhook — called by Keycloak SPI to send SMS via Exotel ──────────────────
 +
 +app.post('/webhook/send-sms', async (req, res) => {
 +
 +  let { phone, otp, name } = req.body;
 +
 +  if (!phone || !otp) return res.status(400).json({ error: 'phone and otp required' });
 +
 +  // Normalize phone to +91XXXXXXXXXX
 +
 +  if (!phone.startsWith('+')) phone = '+91' + phone.replace(/^0+/, '');
 +
 +  const sid   = process.env.EXOTEL_SID       || 'actual sid';
 +
 +  const token = process.env.EXOTEL_API_TOKEN  || 'actual token';
 +
 +  const from  = process.env.EXOTEL_FROM       || 'actual id';
 +
 +  const message = `Dear ${name || 'User'},\n\nOTP for login to your Cotrav Hotel Agent App Is ${otp}\n\nRegards,\nCotrav`;
 +
 +  try {
 +
 +    const url = `https://${sid}:${token}@twilix.exotel.in/v1/Accounts/${sid}/Sms/send`;
 +
 +    const smsRes = await axios.post(url, qs.stringify({ From: from, To: phone, Body: message }), {
 +
 +      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 +
 +      validateStatus: s => s <600,
 +
 +    });
 +
 +    console.log('[Exotel] status:', smsRes.status);
 +
 +    return res.json({ success: true });
 +
 +  } catch (err) {
 +
 +    return res.status(500).json({ error: `Exotel error: ${err.message}` });
 +
 +  }
 +
 +});
 +
 +// ── Start Server ──────────────────────────────────────────────────────────────
 +
 +app.use(express.static(path.join(__dirname, 'public')));
 +
 +app.get('/', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'keycloak-test.html')));
 +
 +app.listen(3000, () => console.log('Server running on http://localhost:3000'));
 +
 +</code>
 +==== Upload server.js from local machine (run on Windows): ====
 <code> <code>
 +
 scp d:/keycloak-test/server.js root@64.227.190.56:/root/keycloak-test/server.js scp d:/keycloak-test/server.js root@64.227.190.56:/root/keycloak-test/server.js
  
 </code> </code>
 +
  
 ==== Start Node.js server: ==== ==== Start Node.js server: ====
authentication/send_sms.1773041200.txt.gz · Last modified: by sonali