security

Practical Application Security for Developers - Part 6: Passwordless Login

Compare magic links, OTP, passkeys, and WebAuthn with practical implementation guidance for passwordless login.

Reading Time: 10 min readAuthor: DeepTechHub
#security#appsec#passwordless#webauthn#passkeys
Practical Application Security for Developers - Part 6: Passwordless Login

Practical Application Security for Developers - Part 6: Passwordless Login

In Part 5 we covered SSO, OAuth 2.0, and OpenID Connect. Those protocols tell us how identity moves between applications, but they do not force us to keep using passwords as the front door.

That matters because passwords remain one of the weakest links in modern systems. Users reuse them, phishers steal them, bots spray them, and support teams burn hours resetting them. Passwordless login is attractive not because it is trendy, but because it removes a credential humans handle badly.

This article looks at the passwordless approaches developers are most likely to implement:

  1. Magic links
  2. One-time passwords (OTP)
  3. WebAuthn and passkeys
  4. When each option works well
  5. The implementation details that separate usable systems from insecure ones

What Passwordless Really Means

Passwordless does not mean "less security." It means the user proves identity through a different factor:

  • Something they have: phone, email inbox, security key
  • Something they are: biometric unlock on a trusted device
  • A cryptographic key pair stored by the authenticator

The big shift is that your system stops relying on a long-lived shared secret typed by a human.

That changes the threat model. You reduce password reuse and credential stuffing, but you still need to think about:

  • Account recovery
  • Device loss
  • Email compromise
  • SMS interception
  • Replay prevention
  • Session hardening after login

A Practical Decision Framework

Not every passwordless option fits every product.

MethodBest forMain risk
Magic linksLow-friction consumer loginEmail inbox becomes the real credential
Email or SMS OTPSimple verification and step-up authPhishing, SIM swap, delivery delays
Authenticator-app OTPMFA and stronger possession checksUser setup friction
Passkeys / WebAuthnHigh-security, low-phishing loginRecovery and device portability planning

Rule of thumb: If you want the strongest phishing resistance, use passkeys. If you want the fastest rollout for a consumer product, start with magic links or email OTP, but do not pretend they are equivalent to WebAuthn.


Magic Links: Frictionless, but Only as Secure as Email

Magic links are popular because they remove almost all user effort. A person enters an email address, your application sends a time-limited link, and clicking it completes login.

That works well for:

  • B2C products where convenience is critical
  • Occasional-use apps where users forget passwords
  • Early-stage SaaS products trying to reduce sign-up abandonment

How the Flow Works

  1. User submits email address
  2. Server generates a one-time login token
  3. Server stores or hashes metadata for later validation
  4. Server emails a short-lived login URL
  5. User clicks the link
  6. Server verifies token, checks expiry and one-time-use state, then creates a session

Important Security Detail: Hide Account Existence

The response should be the same whether or not the email exists.

If an account exists, we have sent a sign-in link.

That prevents email enumeration attacks, where attackers probe your login form to discover valid user accounts.

Token Design

The token should be:

  • High-entropy and unpredictable
  • Short-lived, typically 5 to 15 minutes
  • Single-use
  • Bound to a login intent in your database or cache

Safer Storage Pattern

Do not store raw magic-link tokens if you can avoid it. Store a hash and compare hashes on receipt, the same way you would avoid storing raw API secrets.

Example Login URL

https://example.com/auth/magic?token=7b2f3ef5f9c5404fb8f2d3fd8e42f8f6...

Java Example: Token Issue Service

import java.security.SecureRandom;
import java.util.Base64;
 
public class MagicLinkTokenService {
 
    public static String generateToken() {
        byte[] bytes = new byte[32];
        new SecureRandom().nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
}

What to Store Per Request

FieldWhy
User ID or emailMap token to account
Token hashAvoid storing raw token
Expiration timeEnforce short validity
Used flag / consumed timestampEnforce one-time use
Request metadataThrottle and investigate abuse

Magic Link Mistakes to Avoid

  1. Stateless reusable links - If you do not track use, replay becomes possible.
  2. Long-lived links - A 24-hour login link is an account takeover opportunity.
  3. Logging the full URL - The token in the query string is a credential.
  4. No rate limiting - Attackers can flood inboxes or test many accounts.

OTP Login: Familiar, Flexible, and Easy to Misjudge

One-time passwords are everywhere because they are easy to explain to users and easy to implement incrementally. But not all OTP delivery methods are equal.

Common OTP Channels

ChannelStrengthWeakness
Email OTPEasy rolloutSame trust boundary as email inbox
SMS OTPWorks broadlySIM swap and interception risk
Authenticator app OTPStronger, offline capableUser enrollment required

OTP Login Flow

  1. User enters email or phone number
  2. Server generates a random one-time code
  3. Server stores only a hashed representation if possible
  4. Code is delivered to the user
  5. User submits the code
  6. Server verifies correctness, expiry, and single-use status
  7. Server creates a session or issues tokens

OTP Generation Requirements

Use a cryptographically secure random source. Do not use java.util.Random.

import java.security.SecureRandom;
 
public class OtpUtil {
 
    private static final SecureRandom RANDOM = new SecureRandom();
 
    public static String generateSixDigitOtp() {
        int value = 100000 + RANDOM.nextInt(900000);
        return Integer.toString(value);
    }
}

OTP Validation Controls

  • Expire codes quickly, often within 30 seconds to 5 minutes
  • Allow only a small number of attempts
  • Lock or slow down repeated failures
  • Invalidate on successful use
  • Correlate request with device and IP when possible

When OTP Works Best

  • Login verification for low- to medium-risk systems
  • Multi-factor authentication after password or SSO login
  • Account recovery as one step in a broader recovery process

When OTP Is Not Enough

OTP, especially over SMS or email, is not the same as phishing-resistant authentication. For high-value administrative access, payments, or privileged developer tooling, passkeys are a much better target state.


OTP as MFA

OTP is often strongest when used as an additional factor rather than the only one.

Example MFA Flow

  1. User signs in with primary credential or SSO session
  2. Risk engine or policy requires a second factor
  3. OTP is delivered or generated by authenticator app
  4. User submits code
  5. Server verifies and elevates session assurance

This is especially useful for:

  • Finance actions
  • Admin dashboards
  • Password or email change flows
  • Access from new devices or countries

Passkeys and WebAuthn: The Strongest Passwordless Option

If magic links are about convenience, WebAuthn is about cryptographic assurance.

Passkeys and WebAuthn replace shared secrets with public-key cryptography:

  1. The authenticator creates a key pair
  2. The private key stays on the device or security key
  3. The server stores the public key
  4. During login, the server issues a challenge
  5. The authenticator signs the challenge
  6. The server verifies the signature

That design makes WebAuthn highly resistant to phishing and credential replay.

Why It Is Stronger

  • No password to steal or reuse
  • No shared secret sent over the network
  • Authentication is scoped to the relying party origin
  • The private key stays under authenticator control

Terminology That Helps

TermMeaning
Relying Party (RP)Your application or site
AuthenticatorDevice or key that creates and uses the key pair
Credential IDIdentifier for a registered authenticator credential
ChallengeServer-generated random value to prevent replay

WebAuthn Registration Flow

Registration is where the device creates the credential.

  1. User is already identified or in a trusted enrollment flow
  2. Server generates challenge and registration options
  3. Browser calls navigator.credentials.create()
  4. Authenticator creates key pair and returns attestation data
  5. Server verifies attestation and stores credential metadata

Minimal Browser Example

async function registerPasskey(optionsFromServer) {
  const credential = await navigator.credentials.create({
    publicKey: optionsFromServer
  });
 
  return {
    id: credential.id,
    rawId: credential.rawId,
    type: credential.type,
    response: {
      attestationObject: credential.response.attestationObject,
      clientDataJSON: credential.response.clientDataJSON
    }
  };
}

What the Server Should Store

FieldWhy
User IDLink credential to account
Credential IDIdentify which authenticator is used later
Public keyVerify future assertions
Signature counter / metadataDetect suspicious usage patterns
Device or transport metadataOperational visibility

WebAuthn Authentication Flow

Authentication is the reverse path.

  1. User starts login
  2. Server generates a fresh challenge
  3. Browser calls navigator.credentials.get()
  4. Authenticator signs the challenge
  5. Browser returns assertion to server
  6. Server verifies signature using stored public key

Minimal Browser Example

async function authenticatePasskey(optionsFromServer) {
  const assertion = await navigator.credentials.get({
    publicKey: optionsFromServer
  });
 
  return {
    id: assertion.id,
    rawId: assertion.rawId,
    type: assertion.type,
    response: {
      authenticatorData: assertion.response.authenticatorData,
      clientDataJSON: assertion.response.clientDataJSON,
      signature: assertion.response.signature
    }
  };
}

Challenge Endpoint Example in Spring Boot

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
 
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
 
@RestController
public class WebAuthnChallengeController {
 
    @PostMapping("/api/webauthn/authenticate/challenge")
    public Map<String, Object> challenge(@RequestBody Map<String, String> request) {
        byte[] challenge = new byte[32];
        new SecureRandom().nextBytes(challenge);
 
        String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(challenge);
 
        return Map.of(
                "challenge", encoded,
                "timeout", 60000
        );
    }
}

The browser example is intentionally minimal. In a production implementation, you should rely on a server-side library such as WebAuthn4J or the Yubico WebAuthn server to handle verification safely.


Recovery Is the Hard Part of Passwordless

Passwordless systems fail operationally when teams focus only on login and forget recovery.

Ask these questions before rollout:

  • What happens when the user loses the device?
  • Can they register multiple passkeys?
  • Is fallback email recovery strong enough?
  • Do admins have a controlled recovery process?
  • How do you step up verification before changing credentials?

If recovery is weak, attackers will attack recovery instead of login.


Common Mistakes in Passwordless Systems

  1. Treating email as inherently secure - Magic links are only as strong as the inbox.

  2. Generating tokens or OTPs with weak randomness - Always use SecureRandom.

  3. Not enforcing single use - Reusable links and OTPs invite replay.

  4. No rate limiting or anomaly detection - Abuse starts at the request endpoint.

  5. Logging sensitive tokens and codes - Links, OTPs, and session grants are credentials.

  6. Ignoring recovery design - Device loss is guaranteed; plan for it.

  7. Calling SMS OTP phishing-resistant - It is better than nothing, but it is not passkey-grade security.


Quick Decision Guide

ScenarioBest fit
Consumer app optimizing for low frictionMagic links or email OTP
Medium-risk app needing broad compatibilityAuthenticator-app OTP or email OTP
Admin access or developer platformPasskeys / WebAuthn
Step-up verification after SSOOTP or passkey challenge
Long-term strategic authentication directionPasskeys

What's Next

By this point we have covered users, identity providers, and passwordless login. The next question is what happens when there is no user in the loop at all and services need to trust one another across a distributed system.

In Part 7, we will cover service identity, service-to-service authorization, call-chain security, mTLS, OAuth client credentials, and the trade-offs between application-level and infrastructure-level enforcement.

Continue Learning

Explore more guides and resources to deepen your knowledge.