DEV Community

Cover image for How to Build Passwordless Auth That Doesn't Frustrate Users (Magic Links, One-Time Codes, and What Actually Works)
Shola Jegede
Shola Jegede Subscriber

Posted on

How to Build Passwordless Auth That Doesn't Frustrate Users (Magic Links, One-Time Codes, and What Actually Works)

Passwordless auth is supposed to make signing in easier. Here is why most implementations make it worse — and how to get it right.

In this article, you will learn:

  • Why passwordless authentication exists and what problem it actually solves
  • The real difference between magic links and one-time codes — and why Kinde chose OTPs over magic links
  • The UX mistakes that turn passwordless into a frustration machine
  • What the full range of passwordless options looks like: email OTP, SMS OTP, username + code, and social login
  • How to enable and configure each method in Kinde
  • How to set per-app and per-org authentication rules
  • Attack protection settings you should configure before going live
  • What actually works and what to avoid

Let's dive in!

The Problem with Passwords (and Why Replacing Them Is Harder Than It Looks)

Passwords are a disaster. Users reuse them, forget them, choose weak ones, get phished with them, and abandon your signup flow because of them. The average person manages dozens of accounts, and the reality of password hygiene in 2026 is grim: most people use the same five passwords across everything they care about.

So the instinct to go passwordless is correct. Remove the password and you remove a huge class of security problems and UX friction at once. No "Forgot password?" flows. No "Password must contain one uppercase letter, one symbol, and the blood of a dragon." Just an identity and a verification step.

The problem is that most passwordless implementations trade one frustration for another. Instead of a forgotten password, users face a magic link that expired. Instead of a weak password, they face an OTP email that landed in spam. Instead of a clear error message, they face a confusing redirect loop across browser tabs.

Done right, passwordless is a dramatic improvement. Done poorly, it makes users nostalgic for the password box.

Here is how to do it right, and how Kinde structures its passwordless authentication options to actually serve users instead of annoying them.

Side-by-side UX flow — Password flow (Email → Password field →

Magic Links vs One-Time Codes: The Real Difference

Before getting into implementation, it is worth understanding the actual security and UX tradeoff between the two most common passwordless approaches, because they are not equivalent.

A magic link is a URL sent to the user's email that, when clicked, signs them in immediately. One click, no input required.

A one-time passcode (OTP) is a short code — typically 6 digits — sent to the user's email or phone. The user must actively type it into your sign-in page.

Magic links sound more convenient. Fewer steps, lower friction, just click. But Kinde deliberately does not support magic links as a password alternative. Here is the reasoning, and it is worth understanding because it shapes how you think about passwordless security.

Magic links have a single-click attack surface. Anyone with access to the user's email can click the link and be instantly signed in. If the user's email is open on a shared or unlocked device, a family member, colleague, or attacker can click the link without knowing anything else. There is no second factor implied by the act of clicking.

OTPs require active participation. To use an OTP, you have to have initiated the correct sign-in flow on the correct device, and then separately retrieve and type the code. If someone else has access to your email and they see an OTP, they cannot use it unless they are also sitting at the browser tab where the sign-in flow started. If the OTP arrives via SMS, they need the physical device and its unlock code.

This is a meaningful security distinction, especially for B2B and SaaS products where account compromise can have real consequences for the user's organization — not just their personal data.

Kinde's position is clear: OTPs are more secure and still offer a dramatically better user experience than passwords. The OTP flow is a bit more manual than a magic link click, but it is fundamentally more defensible. That is a trade-off worth making.

Security comparison — Magic link attack scenario (Shared device → Attacker sees email → Clicks link → Signed in, no resistance) vs OTP attack scenario (Shared device → Attacker sees email → Gets code → But: must have initiated correct sign-in flow → Cannot use code alone)

What Passwordless Options Does Kinde Support?

Kinde supports four passwordless authentication methods, covering email, phone, and username-based flows. Each method can be enabled independently and configured per application or per organization.

Email + code. The user enters their email address. Kinde sends a one-time passcode to that email. The user enters the code to complete sign-in. This is enabled by default in all new Kinde accounts — it is the recommended starting point for most products.

Phone + code (SMS OTP). The user enters their phone number. Kinde sends a passcode via SMS through Twilio. The user enters the code. This requires a Twilio account and a Kinde Pro plan or above. It is the right choice for products where users are more likely to have reliable phone access than email access, or where mobile-first UX is a priority.

Username + code. The user enters a username instead of an email as their sign-in identity. They still provide an email at signup, but sign in with a username and receive the OTP via that email. This is useful for products where usernames are part of the product identity — gaming platforms, community tools, and developer tools where GitHub-style handles matter.

Social connections. Google, GitHub, Microsoft, Apple, and 10+ other providers. Social login is a form of passwordless authentication — the user delegates the verification step to a provider they already trust. Kinde handles the OAuth flow and the token exchange. Social connections can be combined with email or phone OTP for fallback.

Four quadrant grid showing the four passwordless options — Email + code, Phone + code, Username + code, Social — with a one-line description and the primary use case for each

The Passwordless UX Mistakes That Frustrate Users

Before the implementation steps, a quick catalogue of the things that make passwordless authentication worse than passwords. These are avoidable if you know what to watch for.

Slow code delivery. If the OTP email takes 45 seconds to arrive, the user assumes something broke and refreshes or starts over. Target delivery under 5 seconds. Kinde delivers codes quickly, but your email sender reputation and deliverability setup affect this — see the section on custom email senders below.

Codes that expire too fast. Some implementations set 60-second or 90-second windows. That is often not enough time for the user to switch apps, unlock their phone, and type the code. Kinde's passcodes expire after 2 hours, which is a generous and sensible window that covers the common case of someone signing in, getting distracted, and returning to complete it.

No "resend code" option. Users will not receive their code sometimes. Spam filters, brief mail server delays, a typo in the email — all of these happen. Your sign-in page must have a clearly visible "Resend code" action. If users cannot find it, they assume the product is broken.

Too many authentication choices. Offering six different sign-in methods on one screen creates decision paralysis. Pick the one or two that match your users' context and present them cleanly. You can always add more later.

Unclear error messages. "Authentication failed" tells the user nothing. "That code has expired. Request a new one below." tells them exactly what happened and what to do next. Match the message to the actual failure mode.

Sending codes to a junk folder. This is the most common complaint about OTP-based auth. A code from an unknown sender ("noreply@kinde.com") is more likely to hit spam than a code from your own domain. Configure a custom email sender in Kinde so codes arrive from your domain, not Kinde's.

Enabling Passwordless Authentication in Kinde

Step #1: Access the Authentication Settings

In your Kinde dashboard, navigate to SettingsEnvironmentAuthentication.

Kinde dashboard — Settings > Environment > Authentication page, showing the full authentication options grid with Passwordless and Password sections, and the Social connections section below

You will see three sections: Passwordless, Password, and Social connections. For each authentication type, there is a tile with a Configure button. Note that you cannot enable both passwordless and password authentication for the same application — they are mutually exclusive per app.

Step #2: Enable Email + Code

Email + code is the foundational passwordless method and the one most products should start with. Kinde enables it by default for new accounts, but here is how to confirm and configure it.

Select Configure on the Email tile in the Passwordless section.

The

In the modal that appears, you will see a list of your applications. Toggle email + code on or off for each application independently. This means you can have passwordless on your main app and password auth on a separate admin portal if you need to. Select Save.

Step #3: Enable Phone (SMS) Authentication

Phone authentication is ideal for mobile-first products and audiences where SMS is more reliable than email. It requires a Twilio account and Kinde Pro plan or above.

Select Configure on the Phone tile in the Passwordless section.

The

The

To complete the setup, you will need to connect your Twilio account. Navigate to SettingsTwilio and enter your Twilio account SID, auth token, and the phone number or messaging service SID you want to send from. Once connected, phone auth will work for the applications you toggled on.

Note: SMS delivery is subject to carrier filtering and regional regulations. For international products, test OTP delivery across your target markets before launch. Kinde has documentation on SMS deliverability worth reading before enabling this at scale.

Step #4: Enable Username + Code

If your product uses usernames as the primary identity — rather than email addresses — this method lets users sign in with their username while still receiving OTPs via their registered email.

Select Configure on the Username + code tile in the Passwordless section and toggle it on for the relevant applications.

Note: Username + code requires users to have provided an email at signup. The OTP is sent to that email, not displayed next to the username field. Make sure your sign-in page clearly communicates that the code will go to their registered email.

Step #5: Add Social Connections

Social sign-in is a seamless form of passwordless auth for many users. Google in particular is dominant — a large proportion of users will choose "Continue with Google" over any other option if it is available.

In the Social connections section, select Add connection.

Social connections section in Kinde Authentication settings, showing the

Select the providers you want to add and select Save. After saving, each social connection you added needs to be configured with the provider's credentials — client ID and client secret from the provider's developer console. Select the individual provider tile and follow the setup steps. Kinde has dedicated setup guides for Google, GitHub, Microsoft, Apple, and all other supported providers.

For most consumer SaaS products, Google is the one social connection that should be there from day one. For developer tools, add GitHub. For B2B with Microsoft-heavy enterprise customers, add Microsoft.

Configuring a Custom Email Sender

This is not optional if you care about OTP deliverability. By default, Kinde sends OTPs from its own domain. Emails from unknown senders hit spam filters more often than emails from a sender the user's mail system recognizes.

To configure a custom sender, navigate to SettingsEmail in your Kinde dashboard.

Kinde Settings > Email page showing the custom email sender configuration section with

You can set:

  • From name: The display name users see in their email client (e.g. "YourApp")
  • From address: The email address the code comes from (e.g. auth@yourapp.com)

For the custom address to work, you need to verify the domain with Kinde by adding DNS records that Kinde provides. This typically takes a few minutes to propagate. Once verified, all OTPs sent from Kinde will use your domain.

This single change reduces OTP spam classification significantly. Do it before launch.

Setting Per-App and Per-Org Authentication

One of Kinde's most practical capabilities is the ability to set different authentication methods for different applications and different organizations. This matters more than it seems.

Per-App Authentication

Consider a product that has a standard user-facing application and a separate internal admin portal. The user-facing app should use email OTP for maximum conversion and ease. The internal admin portal should use a password (or even enterprise SSO) for tighter control. These are different risk profiles and different user populations.

In Kinde, every authentication method is configured per application via the toggles in the Configure modals you saw earlier. When you navigate to SettingsAuthentication and open any method, you toggle it on or off independently for each registered application.

The Configure modal for Email passwordless, showing multiple application rows with independent toggles — one app has it on, another has it off

Per-Org Authentication

For B2B products, authentication requirements often differ by customer. An enterprise customer may have strict SSO requirements through Microsoft Entra ID. A startup customer may prefer the simplicity of Google social login. A particularly security-conscious customer may want to require MFA on top of OTP.

In Kinde, navigate to Organizations → select an organization → Authentication to see per-organization authentication overrides.

Organization detail page showing the Authentication tab with options to override global authentication settings for this specific organization

You can set custom authentication experiences per organization without changing the global defaults for other tenants. This is the right architecture for B2B products where customer requirements vary.

Configuring Attack Protection

Passwordless auth is not magic — OTP-based systems have their own attack surfaces. Without rate limiting and brute-force protection, an attacker could try to enumerate codes or flood your users with OTP requests.

Kinde provides built-in attack protection settings. Navigate to SettingsSecurityAttack protection.

Kinde Settings > Security > Attack protection page, showing the configurable fields: sign-in attempt limits, lockout duration, IP-level throttling

The settings you can configure include:

  • Maximum sign-in attempts: How many failed attempts before the account is temporarily locked
  • Lockout duration: How long the lockout lasts after the threshold is hit
  • IP-level throttling: Rate limiting on OTP requests per IP address to prevent OTP flooding

The right settings depend on your user base. A consumer app with millions of casual users needs more lenient lockout settings than a B2B tool with a small, known set of users who all know how to sign in. The defaults are reasonable starting points, but tune them based on your support tickets — if users are getting locked out unexpectedly, loosen the thresholds. If you are seeing OTP abuse, tighten them.

Implementing Passwordless in Your App: The Code Side

Once your Kinde settings are configured, your application needs to handle the authentication flow. Kinde's SDKs abstract away the OTP exchange entirely — you do not build the code-sending, code-verifying, or session-creating logic yourself.

For a Next.js application using Kinde's App Router SDK, passwordless login looks like this:

// app/page.tsx — sign-in page with passwordless entry point
import { LoginLink, RegisterLink } from "@kinde-oss/kinde-auth-nextjs/components";

export default function Home() {
  return (
    <main>
      <h1>Welcome to YourApp</h1>
      <p>Sign in to continue  no password required.</p>

      {/* Kinde handles the entire OTP flow on its hosted auth pages */}
      <LoginLink className="btn btn-primary">
        Sign in
      </LoginLink>

      <RegisterLink className="btn btn-secondary">
        Create account
      </RegisterLink>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

When the user clicks "Sign in", Kinde redirects them to the hosted authentication page where they enter their email, receive their OTP, and complete verification. After successful authentication, Kinde redirects back to your application with a session.

To read the authenticated user's details in a server component:

// app/dashboard/page.tsx — protected page
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const { isAuthenticated, getUser } = getKindeServerSession();

  // Redirect unauthenticated users to the sign-in page
  if (!(await isAuthenticated())) {
    redirect("/api/auth/login");
  }

  const user = await getUser();

  return (
    <div>
      <h1>Dashboard</h1>
      {/* user.given_name, user.email, user.picture are all available */}
      <p>Welcome back, {user?.given_name}.</p>
      <p>Signed in as: {user?.email}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The route handler that manages the Kinde auth endpoints needs to exist in your project. Create it at app/api/auth/[kindeAuth]/route.ts:

// app/api/auth/[kindeAuth]/route.ts
import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";

export const GET = handleAuth();
Enter fullscreen mode Exit fullscreen mode

That single line handles login, logout, callback, and registration routes for your entire Kinde integration. No custom token verification, no session management, no OTP exchange logic — Kinde handles it all.

And the middleware that protects your routes:

// middleware.ts
import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";

export default withAuth(
  async function middleware(req) {
    // Optional: add custom logic using req.kindeAuth
  },
  {
    // Public routes that do not require authentication
    publicPaths: ["/", "/about", "/pricing"],
  }
);

export const config = {
  matcher: [
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
  ],
};
Enter fullscreen mode Exit fullscreen mode

With this setup, users who try to access protected routes are automatically redirected to Kinde's passwordless sign-in page. After authenticating with their OTP, they are brought back to the protected page they originally tried to reach.

Testing Your Passwordless Flow

Before going live, run through this checklist:

Test the happy path. Enter a valid email, receive the code, enter it, confirm you land on the right page post-login. Do this in an incognito window to avoid cached sessions.

Test OTP email delivery. Check whether codes arrive promptly (under 10 seconds). Check your spam folder with the default Kinde sender. Then configure your custom sender and check again.

Test code expiry. Wait more than 2 hours without using the code, then try to use it. Confirm the error message is clear and offers a way to get a new code.

Test invalid codes. Enter a wrong code several times. Confirm the error message is clear and the account lockout behaviour matches your attack protection settings.

Test on mobile. Auth flows behave differently on mobile — especially when switching between the email app and the browser. Confirm the redirect back to your app works correctly on iOS and Android.

Test the "Resend code" path. If a user requests a second code, the first code should be invalidated. Confirm this is the case.

Kinde has a dedicated testing section in the docs that covers testing passwordless flows specifically, including test user setup for development environments. Reference the testing documentation at docs.kinde.com before launch.

Passwordless Methods: Comparison

Method Best for Requires Plan
Email + code Most products, all audiences Nothing extra All plans
Phone + code Mobile-first, SMS-comfortable users Twilio account Kinde Pro+
Username + code Community/dev tools with usernames Nothing extra All plans
Social (Google, GitHub, etc.) Consumer apps, developer tools Provider app setup All plans

Conclusion

In this article, you learned why passwordless authentication is worth doing, what separates a good implementation from a frustrating one, and why Kinde deliberately chose OTPs over magic links. You also saw how to enable and configure every passwordless option Kinde supports — email codes, SMS codes, username codes, and social login — along with attack protection, custom email senders, per-app settings, and per-org overrides.

The pattern that actually works is simple: pick the fewest methods that genuinely serve your users, configure custom email sending before launch, write clear error messages, and let Kinde handle the OTP exchange so you can focus on your product.

Passwords are yesterday's problem. Create a free Kinde account today and replace them with an authentication flow your users will actually enjoy.

Top comments (0)