Client Area

Mobile App Auth Strategy — JWT, OAuth, Magic Links, Passkeys

ByDomain India Team·DomainIndia Engineering
7 min read24 Apr 20263 views
# Mobile App Auth Strategy — JWT, OAuth, Magic Links, Passkeys
TL;DR
Pick the right auth method for your mobile app. This guide compares classic email+password with JWT, OAuth social login, magic links (email), OTP via WhatsApp/SMS, and passkeys (the 2026 password-less future) — with integration code for your DomainIndia-hosted backend.
## The six methods
MethodUser frictionSecurityIndia fitComplexity
Email + password + JWTMediumMediumGoodLow
OAuth (Google/Apple)LowHighGoodMedium
Magic link (email)LowMediumSlow email deliveryLow
OTP via SMSLowMediumGoodMedium (pay per SMS)
OTP via WhatsAppLowMediumExcellentMedium
Passkeys (WebAuthn)LowestHighestGrowingHigh
Most mobile apps in 2026 offer 2-3 methods — users pick. Don't force one. ## Method 1 — Email + password + JWT Classic. Full flow: ``` Signup: POST /auth/signup { email, password, name } Login: POST /auth/login { email, password } → { accessToken, refreshToken } Later: GET /api/me Authorization: Bearer Refresh: POST /auth/refresh { refreshToken } → { accessToken, refreshToken } ``` Server (Node.js) — hash passwords with bcrypt, issue short-lived JWT: ```typescript import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; app.post('/auth/signup', async (req, res) => { const { email, password, name } = req.body; const hash = await bcrypt.hash(password, 12); const user = await db.user.create({ data: { email, passwordHash: hash, name } }); const tokens = issueTokens(user); res.json({ user: { id: user.id, email, name }, ...tokens }); }); app.post('/auth/login', async (req, res) => { const { email, password } = req.body; const user = await db.user.findUnique({ where: { email } }); if (!user || !await bcrypt.compare(password, user.passwordHash)) { return res.status(401).json({ error: 'Invalid credentials' }); } const tokens = issueTokens(user); res.json({ user: { id: user.id, email: user.email, name: user.name }, ...tokens }); }); ``` See our [JWT Security Best Practices](https://domainindia.com/support/kb/jwt-security-best-practices-replay-refresh-revocation) for the details. **Mobile client storage:** - iOS: Keychain Services - Android: Keystore - Never: SharedPreferences (can be extracted with root) ## Method 2 — OAuth / Sign in with Google / Apple On iOS, Apple requires Sign in with Apple if you offer any social login. On Android, Sign in with Google is near-universal. ### Google Sign-In (Android) ```kotlin val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(YOUR_SERVER_CLIENT_ID) .requestEmail() .build() val client = GoogleSignIn.getClient(this, gso) val intent = client.signInIntent startActivityForResult(intent, RC_SIGN_IN) // In onActivityResult: val task = GoogleSignIn.getSignedInAccountFromIntent(data) val account = task.getResult(ApiException::class.java) val idToken = account.idToken // Send to your backend: api.googleSignIn(idToken) ``` ### Sign in with Apple (iOS) ```swift let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.email, .fullName] ASAuthorizationController(authorizationRequests: [request]).performRequests() // In delegate: let credential = authorization.credential as? ASAuthorizationAppleIDCredential let tokenData = credential?.identityToken let idToken = String(data: tokenData!, encoding: .utf8) // Send to backend ``` ### Your backend verifies ```typescript import { OAuth2Client } from 'google-auth-library'; const client = new OAuth2Client(GOOGLE_CLIENT_ID); app.post('/auth/google', async (req, res) => { const ticket = await client.verifyIdToken({ idToken: req.body.idToken, audience: GOOGLE_CLIENT_ID, }); const { email, name, sub: googleId } = ticket.getPayload(); let user = await db.user.findUnique({ where: { email } }); if (!user) { user = await db.user.create({ data: { email, name, googleId, provider: 'google' }, }); } const tokens = issueTokens(user); res.json({ user, ...tokens }); }); ``` Apple: use `apple-signin-auth` npm package to verify the Apple JWT against their JWK. ## Method 3 — Magic link (email) Passwordless via email. Great for low-friction signups. ```typescript app.post('/auth/magic-link', async (req, res) => { const { email } = req.body; let user = await db.user.findUnique({ where: { email } }); if (!user) user = await db.user.create({ data: { email } }); const token = crypto.randomBytes(32).toString('hex'); await redis.setex(`magic:${token}`, 900, user.id); // 15 min const link = `${FRONTEND_URL}/auth/verify?token=${token}`; await sendEmail(email, 'Your login link', ` Click to log in: ${link} Expires in 15 minutes. Didn't request this? Ignore. `); res.json({ message: 'Check your email' }); }); app.get('/auth/verify', async (req, res) => { const userId = await redis.get(`magic:${req.query.token}`); if (!userId) return res.status(400).send('Expired or invalid link'); await redis.del(`magic:${req.query.token}`); const user = await db.user.findUnique({ where: { id: userId } }); const tokens = issueTokens(user); // Return tokens to client somehow (web: cookie; mobile: deep link) }); ``` Issue: email delivery takes 5-30 seconds. Frustrating for users expecting instant. ## Method 4 — OTP via WhatsApp (India-favoured) WhatsApp has 500M+ Indian users. Delivers in <2 seconds vs 30s for email. Much cheaper than SMS (₹0.12 vs ₹0.30 per message). See our [WhatsApp Business API article](https://domainindia.com/support/kb/whatsapp-business-api-integration-indian-businesses) for setup. ```typescript app.post('/auth/otp/send', async (req, res) => { const { phone } = req.body; // +919876543210 const otp = Math.floor(100000 + Math.random() * 900000).toString(); await redis.setex(`otp:${phone}`, 600, otp); await sendWhatsApp(phone, 'otp_login', [ { type: 'body', parameters: [{ type: 'text', text: otp }] }, ]); res.json({ message: 'OTP sent to WhatsApp' }); }); app.post('/auth/otp/verify', async (req, res) => { const { phone, otp } = req.body; const stored = await redis.get(`otp:${phone}`); if (!stored || stored !== otp) { return res.status(400).json({ error: 'Invalid OTP' }); } await redis.del(`otp:${phone}`); let user = await db.user.findUnique({ where: { phone } }); if (!user) user = await db.user.create({ data: { phone } }); const tokens = issueTokens(user); res.json({ user, ...tokens }); }); ``` Rate limit OTP sending — max 3 per phone per hour, max 10 per IP per hour. ## Method 5 — Passkeys (WebAuthn) — the future Passkeys use the device's biometric (FaceID, fingerprint) instead of passwords. Syncs across devices via iCloud / Google Password Manager. Benefits: - No passwords to remember, steal, or leak - Phishing-proof — tied to domain - Approved by Google, Apple, Microsoft as the future Adoption in 2026 is growing fast; ~30% of iOS users have passkeys set up. ```typescript // Backend — generate challenge import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'; app.post('/auth/passkey/register/begin', async (req, res) => { const user = await getUser(req); const options = await generateRegistrationOptions({ rpName: 'Your Company', rpID: 'yourcompany.com', userID: user.id, userName: user.email, attestationType: 'none', authenticatorSelection: { userVerification: 'preferred' }, }); await redis.setex(`passkey-challenge:${user.id}`, 300, options.challenge); res.json(options); }); app.post('/auth/passkey/register/verify', async (req, res) => { const user = await getUser(req); const expectedChallenge = await redis.get(`passkey-challenge:${user.id}`); const verification = await verifyRegistrationResponse({ response: req.body, expectedChallenge, expectedOrigin: 'https://yourcompany.com', expectedRPID: 'yourcompany.com', }); if (verification.verified) { await db.passkey.create({ data: { userId: user.id, credentialId: verification.registrationInfo.credentialID, publicKey: verification.registrationInfo.credentialPublicKey, }, }); } res.json({ verified: verification.verified }); }); ``` Mobile support: React Native + Flutter have plugins. Swift/Kotlin have native APIs. ## Hybrid strategy (real-world recommendation) Offer: 1. **OTP via WhatsApp** as primary (lowest friction for India) 2. **Sign in with Google + Apple** as social option 3. **Email + password** as traditional fallback 4. **Passkey setup** offered after first login as upgrade Don't force any single method. ## Session management across methods All methods end the same way — issue access + refresh token. Backend treats them identically post-auth. Track auth method for analytics: ```typescript await db.loginEvent.create({ data: { userId, method: 'whatsapp_otp', ip: req.ip, userAgent: req.headers['user-agent'] }, }); ``` ## Rate limiting (critical) Every auth endpoint is brute-forcable. Rate limit: ```typescript import rateLimit from 'express-rate-limit'; const loginLimit = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, message: 'Too many attempts', }); app.post('/auth/login', loginLimit, loginHandler); ``` For OTPs, also rate limit per phone number (not just IP). ## Common pitfalls ## FAQ
Q OAuth or OTP for Indian apps?

Both. OTP (WhatsApp) for users who don't use Google often. OAuth for the tech-forward segment.

Q How long should JWT tokens live?

Access: 15-60 min. Refresh: 7-30 days. Passkey session: up to platform OS enforces re-auth.

Q Biometric auth in the app — isn't that the same as passkeys?

App biometric unlocks local data (e.g. stored refresh token). Passkeys are a server-verified credential. Different layers.

Q Can I use Firebase Auth + custom backend?

Yes — Firebase Auth issues ID tokens; your backend verifies via firebase-admin. Saves you implementing auth but adds Firebase dependency.

Q Sign in with Email only (no password)?

That IS magic links. Valid pattern; just slow for users waiting on email.

Build multi-method mobile auth on a DomainIndia backend. See our VPS plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket

Mobile Auth — JWT, OAuth, Magic Links, WhatsApp OTP, Passkeys