पासवर्ड का जमाना हुआ पुराना: WebAuthn और Passkeys का जादू
दोस्तों, जरा सोचिए कि एक ऐसी दुनिया हो जहां आपको लॉगिन करने के लिए कोई पासवर्ड याद न रखना पड़े। न ही किसी OTP (One-Time Password) के आने का इंतजार करना पड़े और न ही "Forgot Password" पर क्लिक करके बार-बार नया पासवर्ड बनाना पड़े। कितना शानदार होगा न? आज हम और आप मिलकर इसी जादुई टेक्नोलॉजी को अपने प्रोजेक्ट्स में उतारने वाले हैं।
आज के इस बेहद खास और प्रैक्टिकल मास्टर-क्लास ट्यूटोरियल में हम सीखेंगे कि कैसे हम अपने MERN Stack एप्लीकेशन में Passkeys (WebAuthn) की मदद से Passwordless Authentication सिस्टम को पूरी तरह से स्क्रैच से लागू कर सकते हैं।
एक सीनियर डेवलपर के तौर पर, मैं आपको बता दूँ कि पासवर्ड्स न सिर्फ यूजर्स के लिए सिरदर्द हैं, बल्कि सिक्योरिटी के लिहाज से भी सबसे कमजोर कड़ी हैं। Phishing Attacks, Brute Force, और Data Breaches से निपटने के लिए FIDO Alliance और World Wide Web Consortium (W3C) ने मिलकर WebAuthn API को तैयार किया है। इसे ही हम आमतौर पर Passkeys कहते हैं।
तो चलिए, अपनी चाय का कप उठा लीजिए, VS Code खोलिए और मेरे साथ मिलकर इस बेहतरीन सिक्योरिटी फीचर को स्टेप-बाय-स्टेप कोड करना शुरू करते हैं!
---आखिर ये Passkeys (WebAuthn) काम कैसे करता है?
कोड में सीधे गोता लगाने से पहले, हमारे लिए यह समझना बहुत जरूरी है कि बैकग्राउंड में आखिरकार क्या खिचड़ी पक रही है। जब हम साधारण पासवर्ड सिस्टम का इस्तेमाल करते हैं, तो हम यूजर का पासवर्ड (हैश फॉर्म में) अपने MongoDB डेटाबेस में सेव करते हैं। लेकिन Passkeys पूरी तरह से अलग है। यह Asymmetric Cryptography (Public-Key Cryptography) पर काम करता है।
इसके दो मुख्य फेज होते हैं:
1. Registration Phase (रजिस्ट्रेशन फेज)
- Challenge Generation: सबसे पहले हमारा ExpressJS बैकएंड एक रैंडम "Challenge" जनरेट करता है और उसे क्लाइंट (ब्राउज़र) को भेजता है।
- Key Pair Creation: ब्राउज़र इस Challenge को लेकर डिवाइस के Authenticator (जैसे Windows Hello, Touch ID, Face ID या YubiKey) को देता है। यूजर अपना फिंगरप्रिंट या पिन देता है, जिससे एक नया Cryptographic Key Pair (Public Key और Private Key) बनता है।
- Storing Keys: Private Key हमेशा यूजर के अपने डिवाइस के सिक्योर एंक्लेव में सुरक्षित रहती है (यह कभी भी किसी सर्वर पर नहीं भेजी जाती)। वहीं, Public Key और Credential ID सर्वर (बैकएंड) को वापस भेज दी जाती है, जिसे हम डेटाबेस में सेव कर लेते हैं।
2. Authentication Phase (लॉगिन फेज)
- Challenge Request: जब यूजर लॉगिन करने की कोशिश करता है, तो बैकएंड फिर से एक नया "Challenge" भेजता है।
- Signing the Challenge: यूजर का डिवाइस अपनी सुरक्षित रखी हुई Private Key का इस्तेमाल करके उस Challenge को डिजिटली साइन (Sign) करता है और उसे बैकएंड पर वापस भेजता है।
- Verification: बैकएंड हमारे डेटाबेस में पहले से सेव की गई Public Key का इस्तेमाल करके उस डिजिटल सिग्नेचर को वेरिफाई करता है। अगर वेरिफिकेशन सफल होता है, तो यूजर बिना किसी पासवर्ड के लॉगिन हो जाता है।
ध्यान देने वाली बात ये है कि इस पूरे प्रोसेस में हैकर्स के पास चुराने के लिए कोई पासवर्ड होता ही नहीं है! अगर हमारा डेटाबेस हैक भी हो जाए, तो हैकर्स को सिर्फ Public Keys मिलेंगी, जिनसे वो यूजर के अकाउंट को एक्सेस नहीं कर सकते।
---प्रोजेक्ट का आर्किटेक्चर और सेटअप
हम इस प्रोजेक्ट को दो हिस्सों में बांटेंगे: एक हमारा NodeJS और ExpressJS का सर्वर होगा, और दूसरा हमारा ReactJS का फ्रंटएंड एप्लीकेशन होगा।
हमें इस सिस्टम को बनाने के लिए सबसे बेहतरीन और स्टैंडर्ड लाइब्रेरी का इस्तेमाल करना है, जिसका नाम है SimpleWebAuthn। यह लाइब्रेरी हमारे काम को बेहद आसान बना देती है।
डिपेंडेंसीज इंस्टॉल करना
अपने टर्मिनल में बैकएंड डायरेक्टरी के अंदर निम्नलिखित कमांड्स चलाएं:
npm install express mongoose cors dotenv express-session @simplewebauthn/server
और फ्रंटएंड डायरेक्टरी के अंदर इस पैकेज को इंस्टॉल करें:
npm install @simplewebauthn/browser axios
---
स्टेप 1: बैकएंड सेटअप और MongoDB स्कीमा (Database Schema Design)
चलिए, सबसे पहले अपने MongoDB मॉडल को डिजाइन करते हैं। यहाँ हमें ध्यान रखना होगा कि एक सिंगल यूजर के पास एक से अधिक ऑथेंटिकेटर डिवाइसेज (जैसे उसका फोन, लैपटॉप और सुरक्षा कीज) हो सकते हैं। इसलिए हम यूजर स्कीमा के अंदर डिवाइसेज की एक एरे (Array) बनाएंगे।
बनाइए models/User.js और उसमें नीचे दिया गया कोड लिखिए:
const mongoose = require('mongoose');
const AuthenticatorSchema = new mongoose.Schema({
credentialID: {
type: String,
required: true,
unique: true
},
credentialPublicKey: {
type: String,
required: true
},
counter: {
type: Number,
required: true,
default: 0
},
transports: [String]
});
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
authenticators: [AuthenticatorSchema]
}, { timestamps: true });
module.exports = mongoose.model('User', UserSchema);
यहाँ counter का काम रीप्ले अटैक्स (Replay Attacks) को रोकना है। हर बार जब यूजर अपनी पासकी का इस्तेमाल करेगा, यह काउंटर बढ़ता जाएगा और सर्वर इसे चेक करेगा।
स्टेप 2: एक्सप्रेस सर्वर और WebAuthn लॉजिक का निर्माण
अब हम अपना मुख्य एक्सप्रेस सर्वर डिजाइन करेंगे। इसके लिए हम server.js बनाएंगे। यहाँ हम express-session का उपयोग करंट चैलेंज को टेम्परेरी मेमोरी में स्टोर करने के लिए करेंगे ताकि वेरिफिकेशन के वक्त हम इसे मैच कर सकें।
नीचे पूरा, प्रोडक्शन-रेडी server.js कोड दिया गया है:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const session = require('express-session');
const dotenv = require('dotenv');
const {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} = require('@simplewebauthn/server');
const User = require('./models/User');
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware Setup
app.use(cors({
origin: 'http://localhost:5173', // हमारा React फ्रंटएंड का URL
credentials: true
}));
app.use(express.json());
// Session Middleware: चैलेंज को स्टोर करने के लिए जरूरी है
app.use(session({
secret: 'super-secure-secret-key-change-this-in-production',
resave: false,
saveUninitialized: true,
cookie: {
secure: false, // डेवलपमेंट में false रखें, प्रोडक्शन (HTTPS) में true करें
httpOnly: true,
maxAge: 300000 // 5 मिनट
}
}));
// MongoDB Connection
mongoose.connect('mongodb://127.0.0.1:27017/passkey_db')
.then(() => console.log('MongoDB database connected successfully!'))
.catch(err => console.error('Database connection error:', err));
// रिलेइंग पार्टी (RP) से जुड़ी सेटिंग्स
const rpID = 'localhost';
const expectedOrigin = 'http://localhost:5173';
// ==========================================
// 1. REGISTRATION ENDPOINTS (रजिस्ट्रेशन)
// ==========================================
// रजिस्ट्रेशन ऑप्शन्स जनरेट करना
app.post('/api/auth/register-challenge', async (req, res) => {
try {
const { username } = req.body;
if (!username) {
return res.status(400).json({ error: 'Username is required' });
}
let user = await User.findOne({ username });
if (!user) {
// अगर यूजर नया है, तो उसे टेम्परेरी क्रिएट कर देते हैं
user = new User({ username });
await user.save();
}
const options = await generateRegistrationOptions({
rpName: 'My MERN Passkey Demo App',
rpID,
userID: Buffer.from(user._id.toString()),
userName: user.username,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
authenticatorAttachment: 'cross-platform' // 'platform' मोबाइल/लैपटॉप के इनबिल्ट ऑथेंटिकेटर के लिए
}
});
// करंट चैलेंज को सेशन में सेव कर लें
req.session.currentChallenge = options.challenge;
req.session.registeringUsername = username;
return res.status(200).json(options);
} catch (error) {
console.error('Error during register challenge generation:', error);
return res.status(500).json({ error: error.message });
}
});
// रजिस्ट्रेशन को वेरिफाई करना
app.post('/api/auth/register-verify', async (req, res) => {
try {
const { body } = req;
const expectedChallenge = req.session.currentChallenge;
const username = req.session.registeringUsername;
if (!expectedChallenge || !username) {
return res.status(400).json({ error: 'Session expired or invalid. Please try again.' });
}
const user = await User.findOne({ username });
if (!user) {
return res.status(404).json({ error: 'User registration context not found.' });
}
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge,
expectedOrigin,
expectedRPID: rpID
});
const { verified, registrationInfo } = verification;
if (verified && registrationInfo) {
const { credentialPublicKey, credentialID, counter } = registrationInfo;
// क्रेडेंशियल आईडी और पब्लिक की को बेस64 स्ट्रिंग में कन्वर्ट करके सेव करें
const newAuthenticator = {
credentialID: Buffer.from(credentialID).toString('base64url'),
credentialPublicKey: Buffer.from(credentialPublicKey).toString('base64url'),
counter,
transports: body.response.transports || []
};
user.authenticators.push(newAuthenticator);
await user.save();
// सेशन क्लियर करें
req.session.currentChallenge = null;
req.session.registeringUsername = null;
return res.status(200).json({ success: true, message: 'Passkey registered successfully!' });
}
return res.status(400).json({ error: 'Verification failed.' });
} catch (error) {
console.error('Error during registration verification:', error);
return res.status(500).json({ error: error.message });
}
});
// ==========================================
// 2. AUTHENTICATION ENDPOINTS (लॉगिन)
// ==========================================
// लॉगिन ऑप्शन्स जनरेट करना
app.post('/api/auth/login-challenge', async (req, res) => {
try {
const { username } = req.body;
const user = await User.findOne({ username });
if (!user || user.authenticators.length === 0) {
return res.status(404).json({ error: 'No user or registered passkeys found.' });
}
const options = await generateAuthenticationOptions({
rpID,
allowCredentials: user.authenticators.map(auth => ({
id: Buffer.from(auth.credentialID, 'base64url'),
type: 'public-key',
transports: auth.transports
})),
userVerification: 'preferred'
});
req.session.currentChallenge = options.challenge;
req.session.loggingInUsername = username;
return res.status(200).json(options);
} catch (error) {
console.error('Error during login challenge generation:', error);
return res.status(500).json({ error: error.message });
}
});
// लॉगिन को वेरिफाई करना
app.post('/api/auth/login-verify', async (req, res) => {
try {
const { body } = req;
const expectedChallenge = req.session.currentChallenge;
const username = req.session.loggingInUsername;
if (!expectedChallenge || !username) {
return res.status(400).json({ error: 'Session expired or invalid.' });
}
const user = await User.findOne({ username });
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
// यूजर के उस स्पेसिफिक ऑथेंटिकेटर को ढूंढें जिससे रिक्वेस्ट आ रही है
const authenticator = user.authenticators.find(
auth => auth.credentialID === body.id
);
if (!authenticator) {
return res.status(400).json({ error: 'Device not registered.' });
}
const verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge,
expectedOrigin,
expectedRPID: rpID,
authenticator: {
credentialID: Buffer.from(authenticator.credentialID, 'base64url'),
credentialPublicKey: Buffer.from(authenticator.credentialPublicKey, 'base64url'),
counter: authenticator.counter
}
});
const { verified, authenticationInfo } = verification;
if (verified && authenticationInfo) {
// डेटाबेस में काउंटर को अपडेट करें
authenticator.counter = authenticationInfo.newCounter;
await user.save();
req.session.currentChallenge = null;
req.session.loggingInUsername = null;
// यहाँ पर आप अपना JWT या एक्सप्रेस सेशन टोकन जनरेट करके यूजर को लॉगिन करा सकते हैं
return res.status(200).json({ success: true, message: 'Logged in successfully! Welcome back.' });
}
return res.status(400).json({ error: 'Login verification failed.' });
} catch (error) {
console.error('Error during authentication verification:', error);
return res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server is running beautifully on http://localhost:${PORT}`);
});
---
स्टेप 3: ReactJS फ्रंटएंड इम्प्लीमेंटेशन
अब समय आ गया है अपने क्लाइंट साइड को तैयार करने का। हम एक सुंदर और सिंपल सा फॉर्म बनाएंगे जहाँ यूजर अपना नाम डालेगा और Passkey को रजिस्टर या फिर लॉगिन कर पाएगा।
हम ब्राउज़र के नेटिव WebAuthn API को सीधे हैंडल करने के बजाय `@simplewebauthn/browser` का उपयोग करेंगे, जो कॉम्प्लेक्स बाइनरी डेटा पार्सिंग को ऑटोमैटिकली संभाल लेता है।
बनाइए src/AuthComponent.jsx और उसमें यह कोड पेस्ट करें:
import React, { useState } from 'react';
import axios from 'axios';
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
// Axios को क्रेडेंशियल्स शेयर करने की अनुमति दें ताकि कुकीज और सेशंस सही से काम करें
axios.defaults.withCredentials = true;
const AuthComponent = () => {
const [username, setUsername] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState('');
// 1. नई पासकी रजिस्टर करने का फंक्शन
const handleRegister = async () => {
try {
setMessage('');
setError('');
if (!username) {
setError('Please enter a username first!');
return;
}
// बैकएंड से रजिस्ट्रेशन के ऑप्शन्स प्राप्त करें
const response = await axios.post('http://localhost:5000/api/auth/register-challenge', { username });
const options = response.data;
// ब्राउज़र का बायोमेट्रिक प्रॉम्प्ट ट्रिगर करें
const localCredential = await startRegistration(options);
// वेरिफिकेशन के लिए क्रेडेंशियल वापस बैकएंड पर भेजें
const verifyRes = await axios.post('http://localhost:5000/api/auth/register-verify', localCredential);
if (verifyRes.data.success) {
setMessage('Boom! Passkey registered and saved successfully.');
}
} catch (err) {
console.error(err);
setError(err.response?.data?.error || err.message || 'Error registering passkey');
}
};
// 2. पासकी से लॉगिन करने का फंक्शन
const handleLogin = async () => {
try {
setMessage('');
setError('');
if (!username) {
setError('Please enter your username to find your passkey!');
return;
}
// बैकएंड से लॉगिन के ऑप्शन्स और चैलेंज प्राप्त करें
const response = await axios.post('http://localhost:5000/api/auth/login-challenge', { username });
const options = response.data;
// बायोमेट्रिक प्रॉम्प्ट से वेरिफिकेशन कराएं
const assertion = await startAuthentication(options);
// वेरिफिकेशन सिग्नेचर को बैकएंड पर भेजें
const verifyRes = await axios.post('http://localhost:5000/api/auth/login-verify', assertion);
if (verifyRes.data.success) {
setMessage('Success! You are logged in with biometric magic.');
}
} catch (err) {
console.error(err);
setError(err.response?.data?.error || err.message || 'Authentication failed');
}
};
return (
🔐 Passkey Auth (WebAuthn)
setUsername(e.target.value)}
style={{
width: '100%',
padding: '12px',
borderRadius: '8px',
border: '1px solid #334155',
backgroundColor: '#1e293b',
color: '#fff',
fontSize: '16px',
outline: 'none',
boxSizing: 'border-box'
}}
/>
{message && (
✔ {message}
)}
{error && (
✖ {error}
)}
);
};
export default AuthComponent;
---
महत्वपूर्ण एज केसेज और डिबगिंग (Edge Cases & Common Errors)
जब भी आप स्थानीय रूप से या प्रोडक्शन में Passkeys पर काम करते हैं, तो कुछ ऐसी समस्याएं आती हैं जो अक्सर नए डेवलपर्स का सिर घुमा देती हैं। चलिए, उनसे निपटने के तरीके पहले ही सीख लेते हैं:
1. HTTPS और localhost की आवश्यकता (Secure Context)
ध्यान देने वाली बात ये है कि WebAuthn सिक्योरिटी के मामले में बहुत सख्त है। यह केवल Secure Contexts में ही काम करता है। डेवलपमेंट के दौरान आप localhost (जैसे http://localhost:5173) का इस्तेमाल कर सकते हैं। लेकिन अगर आप अपने लोकल नेटवर्क पर किसी दूसरे आईपी (जैसे http://192.168.1.5) का उपयोग करके टेस्ट करना चाहेंगे, तो ब्राउज़र में navigator.credentials हमेशा `undefined` शो करेगा और एरर आएगी। प्रोडक्शन में बिना SSL (HTTPS) के यह बिल्कुल भी काम नहीं करेगा।
2. Base64 URL Safe Encoding की समस्या
ऑथेंटिकेटर से मिलने वाले credentialID और credentialPublicKey वास्तव में ArrayBuffer (बाइनरी डेटा) होते हैं। डेटाबेस में इन्हें सीधे स्टोर करना मुश्किल होता है। इसलिए, हमने बैकएंड में उन्हें base64url स्ट्रिंग्स में कन्वर्ट किया है। अगर कभी "Invalid Signature" की समस्या आए, तो एक बार चेक करें कि आपने कनवर्टर कोड सही से लिखा है या नहीं।
3. RP ID (Relaying Party ID) मिसमैच
प्रोटोकॉल के अनुसार, आपकी जो डोमेन है (जैसे example.com), वही आपकी rpID होनी चाहिए। अगर आप `localhost` पर काम कर रहे हैं, तो `rpID` को `localhost` ही रखें। अगर इसमें एक भी अक्षर का मिसमैच होगा, तो ब्राउज़र ऑथेंटिकेशन प्रक्रिया शुरू करने से इंकार कर देगा।
परफॉर्मेंस और स्केलेबिलिटी के लिए बेस्ट प्रैक्टिसेज
- मल्टीपल डिवाइसेज का सपोर्ट दें: यूजर्स को अपने प्रोफाइल सेटिंग्स में जाकर एक से अधिक डिवाइसेज (जैसे आईफोन और मैकबुक) को रजिस्टर करने की अनुमति दें ताकि अगर उनका एक डिवाइस खो जाए, तो वे दूसरे से लॉगिन कर सकें।
- बैकअप लॉगिन मेथड: हमेशा एक सेकेंडरी ऑथेंटिकेशन मेथड (जैसे ईमेल ओटीपी या बैकअप रिकवरी कोड्स) जरूर रखें, क्योंकि सभी ब्राउज़र्स या ऑपरेटिंग सिस्टम्स के पास हमेशा अनुकूल हार्डवेयर या सक्षम बायोमेट्रिक्स नहीं होते।
- सुरक्षित कुकीज का उपयोग करें: सेशंस और टोकन्स के ट्रांसफर के लिए कुकीज को हमेशा
httpOnly,secure: true, औरsameSite: 'strict'सेटिंग्स के साथ कॉन्फ़िगर करें।
तो दोस्तों, हमने आज क्या सीखा?
आज हमने बेहद रोमांचक सफर तय किया! हमने समझा कि पासवर्ड्स को बाय-बाय बोलने का समय क्यों आ गया है और कैसे NodeJS और ReactJS के भीतर WebAuthn API को लागू किया जाता है। हमने एक वर्किंग और सुरक्षित डेटाबेस स्कीमा तैयार किया, जिसमें हम यूजर की पब्लिक की सुरक्षित रख रहे हैं और बायोमेट्रिक ऑथेंटिकेटर की मदद से बिना किसी घिसे-पिटे पासवर्ड के लॉगिन प्रोसेस पूरा कर रहे हैं। इस फीचर को अपने प्रोजेक्ट्स में जरूर ट्राई कीजिए और अपने यूजर्स को एक सुपर-फास्ट और हैक-प्रूफ एक्सपीरियंस दीजिए!
---Frequently Asked Questions (FAQs)
Q1: क्या Passkeys के लिए इंटरनेट होना जरूरी है, और क्या यह सुरक्षित है?
जी हाँ, Passkeys पूरी तरह से सुरक्षित हैं। आपकी Private Key कभी भी आपके डिवाइस से बाहर इंटरनेट पर नहीं जाती है। सर्वर को सिर्फ Public Key मिलती है, जिससे हैकर कभी भी आपके असली डिवाइस या बायोमेट्रिक डेटा को एक्सेस नहीं कर सकते।
Q2: अगर यूजर अपना डिवाइस या फोन खो देता है, तो वह कैसे लॉगिन करेगा?
यह एक बहुत ही सामान्य सवाल है। इसीलिए बेस्ट प्रैक्टिस यह है कि डेवलपर को मल्टीपल डिवाइस रजिस्ट्रेशन की अनुमति देनी चाहिए, या फिर बैकअप लॉगिन विकल्प (जैसे ईमेल पर मैजिक लिंक या वन-टाइम रिकवरी कोड) जरूर रखना चाहिए।
Q3: क्या हम लोकल होस्ट पर बिना SSL (HTTPS) के WebAuthn को टेस्ट कर सकते हैं?
हाँ, 'localhost' को ब्राउज़र्स द्वारा पहले से ही सिक्योर ओरिजिन (Secure Origin) माना जाता है। इसलिए आप स्थानीय स्तर पर http://localhost:5173 पर इसे बिना किसी SSL सर्टिफिकेट के आसानी से टेस्ट कर सकते हैं।
टिप्पणियाँ
एक टिप्पणी भेजें