Getting Chrome to prompt to save password when using AJAX to login

NOTE: This question was heavily edited from its original version. The issue has been greatly simplified.

Similar questions have been asked several times before, in different forms - e.g.,

How can I get browser to prompt to save password?

How does browser know when to prompt user to save password?

However, this question is getting at a very specific aspect of Chrome's functionality, so it is quite different in that regard.

Judging by past answers, it appears that the best approach to getting Chrome to prompt for password saving is to submit the form to a dummy iframe, while actually logging in through AJAX: example. That makes sense to me and so I have been fiddling around with some sample code for a few days now. However, Chrome's behaviour in this regard DOES NOT make sense to me. At all. Hence the question.

For some reason, Chrome will not prompt you to save your password if the form that submits to a dummy iframe is present during/right after onDomReady.

A jsfiddle can be found here, but it's of little use because you must create dummy.html locally to see the behaviour described. So the best way to see this is to copy the full html into your own index.html and then create a dummy.html file too.

Here is the full code for index.html. The three approaches are highlighted as (1), (2), (3). Only (2) ensures that the user is prompted to save their password, and the fact that (3) doesn't work is particular perplexing to me.

<html>
<head>
<title>Chrome: Remember Password</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>

<script type="text/javascript">  

    $(function(){

        function create_login_form()
        {
            $('#login_form').html(
            "<input type='text' name='email'>" +
            "<input type='password' name='password'>" +
            "<input id='login-button' type='submit' value='Login'/>");
        }

        // (1) this does not work. chrome seems to require time after "onDomReady" to process
        // the forms that are present on the page. if the form fields exist while that processing
        // is going on, they will not generate the "Save Password?" prompt.
        //create_login_form();

        // (2) This works. chrome has obviously finished its "work" on the form fields by now so
        // we can add our content to the form and, upon submit, it will prompt "Save Password?"

        setTimeout(create_login_form,500);

    });

</script>
</head>
<body>

<!-- Our dummy iframe where the form submits to -->
<iframe src="dummy.html" name="dummy" style="display: none"></iframe>

<form action="" method="post" target="dummy" id="login_form">

    <!-- (3) fails. form content cannot be present on pageload -->
    <!--
    <input type='text' name='email'>
    <input type='password' name='password'>
    <input id='login-button' type='submit' value='Login'/>      
    -->

</form>

</body> 
</html>

If anyone could possibly explain what's going on here, I would be most appreciative.

EDIT: Note that this "saving password issue" with Chrome has extended to people working with AngularJS, also developed by Google: Github

Answers:

Answer

Starting with Chrome 46 you don't need iframe hacks anymore!

All corresponding Chrome issues have been fixed: 1 2 3

Just make sure that the original login form does not "exist" anymore after a push state or an ajax request by either removing (or hiding) the form or changing it's action url (didn't test but should work too). Also make sure all other forms within the same page point to a different action url otherwise they are considered as login form too.

Check out this example:

<!doctype html>
<title>dynamic</title>
<button onclick="addGeneratedForms()">addGeneratedForms</button>
<script>
function addGeneratedForms(){
  var div = document.createElement('div');
  div.innerHTML = '<form class="login" method="post" action="login">\
    <input type="text" name="username">\
    <input type="password" name="password">\
    <button type="submit">login</button> stay on this page but update the url with pushState\
  </form>';
  document.body.appendChild(div);
}
document.body.addEventListener('submit', function ajax(e){
  e.preventDefault();
  setTimeout(function(){
      e.target.parentNode.removeChild(e.target); // Or alternatively just hide the form: e.target.style.display = 'none';

      history.replaceState({success:true}, 'title', "/success.html");

      // This is working too!!! (uncomment the history.xxxState(..) line above) (it works when the http response is a redirect or a 200 status)
      //var request = new XMLHttpRequest();
      //request.open('POST', '/success.html', true); // use a real url you have instead of '/success.html'
      //request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
      //request.send();
  }, 1);
}, false);
</script>

If you are interested there are further examples in this repo. You can run it with node: node server.js. Maybe also see the comments from this commit: https://github.com/mkurz/ajax-login/commit/c0d9503c1d2a6a3a052f337b8cad2259033b1a58

If you need help let me know.

Answer

After researching this issue thoroughly, here is my final report:

First, the problem highlighted in the question was actually a red herring. I was incorrectly submitting the form to an iframe (which arty highlighted - but I didn't connect the dots). My approach to this issue was based on this example, which some of the other, related answers also referenced. The correct approach can be seen here. Basically, the action of the form should be set to the src of the iframe (which is exactly what @arty suggested).

When you do that, the particular problem highlighted in the question goes away because the page is not reloaded at all (which makes sense - why should the page reload when you're submitting to an iframe? It shouldn't, which should have tipped me off). Anyhow, because the page does not reload, Chrome never asks you to save your password, no matter how long you wait to display the form after onDomReady.

Accordingly, submitting to an iframe (properly) will NEVER result in Chrome asking you to save your password. Chrome will only do so if the page that contains the form reloads (note: contrary to older posts on here, Chrome WILL ask you to save your password if the form was dynamically created).

And so, the only solution is to force a page reload when the form is submitted. But how do we do that AND keep our AJAX/SPA structure intact? Here is my solution:

(1) Divide the SPA into two pieces: (1) For non-logged in users, (2) For logged-in users. This might not be doable for those with bigger sites. And I don't consider this a permanent solution - please, Chrome, please... fix this bug.

(2) I capture two events on my forms: onClickSaveButton and onFormSubmit. For the login form, in particular, I grab the user details on onClickSaveButton and make an AJAX call to verify their information. If that information passes, then I manually call formName.submit() In onFormSubmit, I ensure that the form is not submitted before onClickSaveButton is called.

(3) The form submits to a PHP file that simply redirects to the index.php file

There are two advantages to this approach:

(1) It works on Chrome.

(2) On Firefox, the user is now only asked to save their password if they have successfully logged in (personally I've always found it annoying to be asked to save my pwd when it was wrong).

Here is the relevant code (simplified):

INDEX.PHP

<html>
<head>
<title>Chrome: Remember Password</title>

<!-- dependencies -->
<script type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore.js"></script>
<script type="text/javascript" src="http://backbonejs.org/backbone.js"></script>

<!-- app code -->
<script type="text/javascript" src="mycode.js"></script>
<script>

    $(function(){

        createForm();

    });

</script>

</head>
<body>

    <div id='header'>
        <h1>Welcome to my page!</h1>
    </div>

    <div id='content'>
        <div id='form'>
        </div>
    </div>

</body> 
</html>

MYCODE.JS

function createForm() {

    VLoginForm = Backbone.View.extend({

        allowDefaultSubmit : true,

        // UI events from the HTML created by this view
        events : {
            "click button[name=login]" : "onClickLogin",
            "submit form" : "onFormSubmit"
        },

        initialize : function() {
            this.verified = false;
            // this would be in a template
            this.html = "<form method='post' action='dummy.php'><p><label for='email'>Email</label><input type='text' name='email'></p><p><label for='password'>Password<input type='password' name='password'></p><button name='login'>Login</button></form>";        
        },
        render : function() {
            this.$el.html(this.html);
            return this;
        },
        onClickLogin : function(event) {

            // verify the data with an AJAX call (not included)

            if ( verifiedWithAJAX ) {
                this.verified = true;
                this.$("form").submit();
            }           
            else {
                // not verified, output a message
            }

            // we may have been called manually, so double check
            // that we have an event to stop.
            if ( event ) {
                event.preventDefault();
                event.stopPropagation();
            }

        },
        onFormSubmit : function(event) {
            if ( !this.verified ) {             
                event.preventDefault();
                event.stopPropagation();
                this.onClickLogin();
            }
            else if ( this.allowDefaultSubmit ) {
                // submits the form as per default
            }
            else {
                // do your own thing...
                event.preventDefault();
                event.stopPropagation();
            }
        }
    });

    var form = new VLoginForm();
    $("#form").html(form.render().$el);

}

DUMMY.PHP

<?php
    header("Location: index.php");
?>

EDIT: The answer by mkurz looks promising. Perhaps the bugs are fixed.

Answer

You can get Chrome (v39) to show the password save prompt without reloading. Simply submit the form to an iframe whose src is a blank page that does not have a Content-Type: text/html header (leaving out the header or using text/plain both seem to work).

Don't know if this is a bug, or intended behavior. If former, please keep it quiet :-)

Answer

We can now use experimental API for this: Credential Management API.

Official example

My another answer with code snippet

Answer

I'm using Chrome 32.0. Both methods work fine here, can't understand why it is different in your end...

That said, I've often had to use a setTimeout() for a few hundred milliseconds to make e.g. focus() work as expected. That would probably do the trick for you as well. In your case:

setTimeout(display_login_form(),500)

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.