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

29,111

Solution 1

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.

Solution 2

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.

Solution 3

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

Solution 4

I'm using SPA application and had the same issue how to force Chrome to store user credentials if they are passed via AJAX request only. So I used navigator.credentials to invoke default Chrome dialog for credential storage for site. It worked like a charm!

Skinny example.

HTML:

<form id="credential-form">
    <input type="text" name="username" required autocomplete="username">
    <input type="password" name="password" required autocomplete="current-password">
</form>

JS:

let credentialForm = document.getElementById('credential-form');
let credential = new PasswordCredential(credentialForm);
navigator.credentials.store(credential);

For full example please see here where I found this solution.

Solution 5

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

Official example

My another answer with code snippet

Share:
29,111
EleventyOne
Author by

EleventyOne

artificial life n00b... again.

Updated on October 01, 2020

Comments

  • EleventyOne
    EleventyOne over 3 years

    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

  • EleventyOne
    EleventyOne over 10 years
    Did you create the file dummy.html? If that file doesn't exist, it will work. I'm using Chrome version 32.0.1700.76 m. Perhaps it's just me, but the idea of getting it to work by using setTimeout seems like a very shaky solution, prone to the occasional failure. Shouldn't hooking into jQuery's "onDomReady" event be enough?
  • EleventyOne
    EleventyOne over 10 years
    I have updated the question after simplifying the issue greatly.
  • Admin
    Admin over 9 years
    For anyone else who stumbles across this, I think they are releasing a fix for it: code.google.com/p/chromium/issues/detail?id=357696
  • dat
    dat over 8 years
    Thanks for taking the time to do this. The situation has changed on quasi-monthly basis for a couple of years now -- it's good to have a current concise update on what works...
  • Jake Feasel
    Jake Feasel over 8 years
    As a note, it seems like Chrome won't offer this prompt to save credentials when using a non-trusted SSL certificate. Hopefully that helps other devs out there banging their head against the desk, trying to work this out.
  • pmrotule
    pmrotule about 8 years
    I tested it by changing its action url and it works on Chrome. Thanks a lot!
  • Renra
    Renra about 7 years
    This whole thing makes me think it would be much much easier if browsers simply offered us an API for remembering the credentials and credit cards.
  • Jorge Lazo
    Jorge Lazo over 5 years
    @pmrotule are you sure that still works? I tried it out on chrome 69 and it didn't work by changing the action attribute, only if I hid the form or navigate away
  • Cito
    Cito about 5 years
    Note that if you don't want to navigate away, you can use something like history.replaceState(history.state, 'Login').
  • Pi Da
    Pi Da almost 3 years
    Thanks! I was able to solve this issue in AngularDart by setting my form to <form (ngSubmit)="onLogin()">…</form> and calling replaceState() in the form's event-handler: void onLogin() async { … window.history.replaceState(null, '', null); … }