Backend APIs for Mobile AppsAdvanced
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
## The six methods
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
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.
| Method | User friction | Security | India fit | Complexity |
|---|---|---|---|---|
| Email + password + JWT | Medium | Medium | Good | Low |
| OAuth (Google/Apple) | Low | High | Good | Medium |
| Magic link (email) | Low | Medium | Slow email delivery | Low |
| OTP via SMS | Low | Medium | Good | Medium (pay per SMS) |
| OTP via WhatsApp | Low | Medium | Excellent | Medium |
| Passkeys (WebAuthn) | Lowest | Highest | Growing | High |
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