<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/">
	<channel>
		<title>Better Login Forms Without Security Risks | Infinum</title>
		<atom:link href="https://infinum.com/blog/better-login-forms-without-security-risks/feed/" rel="self" type="application/rss+xml" />
		<link>https://infinum.com/blog/better-login-forms-without-security-risks/</link>
		<description>Building digital products</description>
		<lastBuildDate>Fri, 24 Apr 2026 14:02:14 +0000</lastBuildDate>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>

					<item>
				<image>
					<url>7908https://infinum.com/uploads/2017/02/better-login-forms-without-security-risks-0.webp</url>
				</image>
				<title>Better Login Forms Without Security Risks</title>
				<link>https://infinum.com/blog/better-login-forms-without-security-risks/</link>
				<pubDate>Tue, 29 Nov 2016 15:10:00 +0000</pubDate>
				<dc:creator>Damir Svrtan</dc:creator>
				<guid isPermaLink="false">https://infinum.com/the-capsized-eight/better-login-forms-without-security-risks/</guid>
				<description>
					<![CDATA[<p>However, more often than not, various implementations trade-off good UX for security assumptions that are out of place.</p>
<p>The post <a href="https://infinum.com/blog/better-login-forms-without-security-risks/">Better Login Forms Without Security Risks</a> appeared first on <a href="https://infinum.com">Infinum</a>.</p>
]]>
				</description>
				<content:encoded>
					<![CDATA[<div
	class="wrapper"
	data-id="es-199"
	 data-animation-target='inner-items'>
		
			<div class="wrapper__inner">
			<div class="block-blog-content js-block-blog-content">
	
<div class="block-blog-content-sidebar" data-id="es-92">
	</div>

<div class="block-blog-content-main">
	
<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-95"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-93">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-94'
	>
	It goes without saying that good login and registration forms are essential to providing a great user experience.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-98"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-96">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-97'
	>
	However, more often than not, various implementations trade-off good UX for security assumptions that are out of place.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-101"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-media">
	<div	class="media block-media__media media__border--none media__align--center-center"
	data-id="es-99"
	 data-media-type='image'>

	<figure class="image block-media__image-figure image--size-auto" data-id="es-100">
	<picture class="image__picture block-media__image-picture">
												<img
					src="https://infinum.com/uploads/2017/02/better-login-forms-without-security-risks-1.webp"
					class="image__img block-media__image-img"
					alt=""
										height="543"
															width="1000"
										loading="lazy"
					 />
					</picture>

	</figure></div></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-104"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-102">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-103'
	>
	Let’s take a look at 3 easy steps to improve your authentication UX and comment on how to tweak the defaults set in <a href="https://github.com/plataformatec/devise">Devise</a>, the most popular out-of-the-box authentication solution in Rails.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-107"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-105">
	<h2	class='typography typography--size-52-default js-typography block-heading__heading'
	data-id='es-106'
	>
	Get rid of the ambiguous login failure message</h2></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-110"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-108">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-109'
	>
	Usually, login forms show the same error message upon a failed login attempt, regardless of whether you’ve mistyped the email or the password:</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-112"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-code">
	<pre class="phiki language-php github-light" data-language="php" style="background-color: #fff;color: #24292e;"><code><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #005cc5;">Invalid</span><span class="token"> </span><span class="token" style="color: #005cc5;">email</span><span class="token"> </span><span class="token" style="color: #005cc5;">address</span><span class="token" style="color: #d73a49;">/</span><span class="token" style="color: #005cc5;">password</span><span class="token"> </span><span class="token" style="color: #005cc5;">combination</span><span class="token" style="color: #d73a49;">.</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span></code></pre></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-115"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-113">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-114'
	>
	I’ve personally changed multiple emails through the last couple of years so sometimes I don’t know which email I’ve used on a certain service (mostly on services I haven’t used for some time or don’t have them in my LastPass vault yet). The process of getting the right email and password combination can get downright irritating, so oftentimes I resort to trying to send myself a password reset email to the right email address.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-118"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-116">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-117'
	>
	A better approach for websites would be to say <strong>which of the two is actually wrong</strong>:</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-121"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-119">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-120'
	>
	– An account with the given email address does not exist.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-124"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-122">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-123'
	>
	– The given password mismatches the one stored for that particular user.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-127"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-125">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-126'
	>
	If the security skeptic inside you now thinks this is a <strong>obvious security issue</strong> since the internet villains could easily find out which emails are in use and which are not, you’re somewhat wrong:</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-130"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-128">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-129'
	>
	<strong>You probably have a user registration form, so it’s really easy to find out if an email is in use by trying to register with that email address.</strong></p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-133"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-131">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-132'
	>
	If you still think that this approach is wrong, some of the best-known sites in the world use it – check out how <a href="https://accounts.google.com/ServiceLogin#identifier">Google</a>, <a href="https://www.netflix.com/us/Login">Netflix</a>, <a href="https://www.facebook.com/login">Facebook</a>, <a href="https://www.pinterest.com/login/">Pinterest</a> or <a href="https://www.eventbrite.com/">Eventbrite</a> handle invalid login attempts.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-136"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-media">
	<div	class="media block-media__media media__border--none media__align--center-center"
	data-id="es-134"
	 data-media-type='image'>

	<figure class="image block-media__image-figure image--size-auto" data-id="es-135">
	<picture class="image__picture block-media__image-picture">
												<img
					src="https://infinum.com/uploads/2016/05/better-login-forms-without-security-risks-2.webp"
					class="image__img block-media__image-img"
					alt=""
										height="374"
															width="410"
										loading="lazy"
					 />
					</picture>

	</figure></div></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-139"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-137">
	<h3	class='typography typography--size-36-text js-typography block-heading__heading'
	data-id='es-138'
	>
	Devise Implementation</h3></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-142"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-140">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-141'
	>
	Devise hands out an easy way to tweak these messages with the I18n internationalization and localization framework, and it’s a configurable YAML file:</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-144"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-code">
	<pre class="phiki language-yaml github-light" data-language="yaml" style="background-color: #fff;color: #24292e;"><code><span class="line"><span class="token" style="color: #22863a;">e</span><span class="token" style="color: #22863a;">n</span><span class="token">:</span><span class="token">
</span></span><span class="line"><span class="token">  </span><span class="token" style="color: #22863a;">d</span><span class="token" style="color: #22863a;">evise</span><span class="token">:</span><span class="token">
</span></span><span class="line"><span class="token">    </span><span class="token" style="color: #22863a;">f</span><span class="token" style="color: #22863a;">ailure</span><span class="token">:</span><span class="token">
</span></span><span class="line"><span class="token">      </span><span class="token" style="color: #22863a;">n</span><span class="token" style="color: #22863a;">ot_found_in_database</span><span class="token">:</span><span class="token"> </span><span class="token" style="color: #032f62;">W</span><span class="token" style="color: #032f62;">e couldn’t find an account for that email address.</span><span class="token">
</span></span><span class="line"><span class="token">      </span><span class="token" style="color: #22863a;">i</span><span class="token" style="color: #22863a;">nvalid</span><span class="token">:</span><span class="token"> </span><span class="token" style="color: #032f62;">S</span><span class="token" style="color: #032f62;">orry, your password was incorrect. Please double-check your password.</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span></code></pre></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-147"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-145">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-146'
	>
	It’s good practice to link back to the registration form after an unsuccessful login occurs due to the email not existing in the database.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-150"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-148">
	<h2	class='typography typography--size-52-default js-typography block-heading__heading'
	data-id='es-149'
	>
	Ditch the password confirmation field</h2></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-153"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-151">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-152'
	>
	Your user conversion rate is proportional to the simplicity of your registration form. Some of the simplest registration forms (excluding social logins with Facebook, Twitter, etc.) only contain three fields: an email, password, and a password confirmation field.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-156"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-154">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-155'
	>
	<strong>Can it get any simpler than that? Sure it can, ditch the password confirmation.</strong></p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-159"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-157">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-158'
	>
	When was the last time your password and password confirmation field mismatched? I think I <strong>never</strong> actually got that error. And if your users manage to mistype their password, it’s easy for them to reset their password since Devise offers that functionality out-of-the-box.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-162"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-160">
	<h3	class='typography typography--size-36-text js-typography block-heading__heading'
	data-id='es-161'
	>
	Devise Implementation</h3></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-165"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-163">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-164'
	>
	This one’s even easier to accomplish – just remove the password confirmation field from your HTML form (<code>views/devise/registration/new.html.erb</code>) and Devise will automatically ignore it since it’s <strong>nil</strong> (unlike when you submit a blank confirmation field through the form, then it’s an empty string).</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-168"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-166">
	<h2	class='typography typography--size-52-default js-typography block-heading__heading'
	data-id='es-167'
	>
	Show if the email exists when resetting your user’s passwords</h2></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-171"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-169">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-170'
	>
	Regardless of whether the email address exists or not, some websites show the same message after requesting a password reset:</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-173"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-code">
	<pre class="phiki language-php github-light" data-language="php" style="background-color: #fff;color: #24292e;"><code><span class="line"><span class="token">
</span></span><span class="line"><span class="token" style="color: #005cc5;">An</span><span class="token"> </span><span class="token" style="color: #005cc5;">email</span><span class="token"> </span><span class="token" style="color: #005cc5;">with</span><span class="token"> </span><span class="token" style="color: #005cc5;">a</span><span class="token"> </span><span class="token" style="color: #005cc5;">password</span><span class="token"> </span><span class="token" style="color: #005cc5;">reset</span><span class="token"> </span><span class="token" style="color: #005cc5;">link</span><span class="token"> </span><span class="token" style="color: #005cc5;">has</span><span class="token"> </span><span class="token" style="color: #005cc5;">been</span><span class="token"> </span><span class="token" style="color: #005cc5;">sent</span><span class="token"> </span><span class="token" style="color: #005cc5;">to</span><span class="token"> </span><span class="token" style="color: #005cc5;">you</span><span class="token" style="color: #d73a49;">.</span><span class="token">
</span></span><span class="line"><span class="token">
</span></span></code></pre></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-176"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-174">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-175'
	>
	The security implication behind this ambiguous message is that we don’t want to show nosy people whether an email is used on our website or not. The reason to ditch this behavior is the same as before – people can still use the registration form to find which emails were already used.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-179"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-177">
	<h3	class='typography typography--size-36-text js-typography block-heading__heading'
	data-id='es-178'
	>
	Devise Implementation</h3></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-182"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-180">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-181'
	>
	By default, Devise shows whether the email address exists or not, so there’s nothing to do here.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-185"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-183">
	<h3	class='typography typography--size-36-text js-typography block-heading__heading'
	data-id='es-184'
	>
	Important Note</h3></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-188"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-186">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-187'
	>
	Showing whether an email address exists on login and forgot password forms isn’t a security risk if you have a registration form showing the exact same thing. However, you might have an invitation-based system and no registration form. In that case, you’re not exposing the user’s email, so the same security assumption doesn’t imply.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-191"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-heading" data-id="es-189">
	<h2	class='typography typography--size-52-default js-typography block-heading__heading'
	data-id='es-190'
	>
	No ambiguous error messages</h2></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-194"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-192">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-193'
	>
	If you want to prevent malicious attacks, don’t try doing it through ambiguous error messages. Instead, use 2FA (<a href="https://github.com/tinfoil/devise-two-factor">devise-two-factor</a>) or throttle your requests with <a href="https://github.com/kickstarter/rack-attack">Rack Attack</a>.</p></div>	</div>

<div
	class="wrapper wrapper__use-simple--true"
	data-id="es-197"
	 data-animation='slideFade' data-animation-target='inner-items'>
		
			<div class="block-paragraph" data-id="es-195">
	<p	class='typography typography--size-16-text-roman js-typography block-paragraph__paragraph'
	data-id='es-196'
	>
	These are three easy wins. If you’ve got any similar ones up your sleeve, feel free to share your comments below.</p></div>	</div>
</div>
</div>		</div>
	</div><p>The post <a href="https://infinum.com/blog/better-login-forms-without-security-risks/">Better Login Forms Without Security Risks</a> appeared first on <a href="https://infinum.com">Infinum</a>.</p>
]]>
				</content:encoded>
			</item>
		
	</channel>
</rss>