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

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

Official example

My another answer with code snippet


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.


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 :-)


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.