36

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

Community
  • 1
  • 1
EleventyOne
  • 7,300
  • 10
  • 35
  • 40
  • Did you try to set the action of the form to the the iframe src? – arty Jan 28 '14 at 23:06
  • @arty The iframe source is just a dummy html file, it has no content. – EleventyOne Jan 29 '14 at 08:45
  • understood, i only can confirm that on my ubuntu chrome Version 31.0.1650.63 only the option (2) works. anyway, it is always a bad practice to implement login from a 'popup' element. The form should be submitted and the page refreshed after the login. It is for reason implemented this way on all popular sites – arty Jan 29 '14 at 09:38
  • 6
    I'm not sure where you got "popup" from. And I obviously disagree that any of this represents a "bad practice" - SPAs and AJAX are here to stay, thankfully :) – EleventyOne Jan 29 '14 at 09:56
  • 1
    Maybe this bug report for Chrome helps: https://code.google.com/p/chromium/issues/detail?id=282488 – mkurz Jan 31 '14 at 23:41
  • @mkurz If they consider it a bug and fix it then, yes, this whole issue goes away completely (as you no longer need to submit to a dummy iframe). However, if they don't consider it a bug (and I believe Safari has the same behaviour, so they might not consider it one), then this issue remains. – EleventyOne Feb 01 '14 at 02:29
  • Update: it works now with chrome, BUT you have to make sure, that the login form is NOT existent anymore after login (hiding the form is not enough!). But you can ajax, can handle everything in js (no submit needed) and you can stay on the same page without reload. – Daniel Apps Aug 16 '18 at 09:35

6 Answers6

28

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.

mkurz
  • 2,658
  • 1
  • 23
  • 35
  • 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... – dat Jan 02 '16 at 01:18
  • 19
    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. – Jake Feasel Jan 15 '16 at 00:03
  • 1
    I tested it by changing its action url and it works on Chrome. Thanks a lot! – pmrotule Mar 02 '16 at 17:08
  • @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 – Jorge Lazo Oct 30 '18 at 20:12
  • 1
    Note that if you don't want to navigate away, you can use something like history.replaceState(history.state, 'Login'). – Cito Feb 17 '19 at 17:02
  • Thanks! I was able to solve this issue in AngularDart by setting my form to
    and calling replaceState() in the form's event-handler: void onLogin() async { … window.history.replaceState(null, '', null); … }
    – Pi Da Jul 28 '21 at 15:40
8

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.

Community
  • 1
  • 1
EleventyOne
  • 7,300
  • 10
  • 35
  • 40
  • For anyone else who stumbles across this, I think they are releasing a fix for it: https://code.google.com/p/chromium/issues/detail?id=357696 –  Dec 18 '14 at 00:35
  • 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. – Renra Mar 22 '17 at 15:42
1

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

ejain
  • 3,456
  • 2
  • 34
  • 52
1

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

Official example

My another answer with code snippet

shameleo
  • 344
  • 2
  • 13
1

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.

Aldis
  • 439
  • 4
  • 10
  • Does not work: `Uncaught TypeError: Failed to construct 'PasswordCredential': 'id' must not be empty.` – mikep May 31 '23 at 10:43
  • Just checked and still works in Chrome. If You are modifying the example, please, verify that `PasswordCredential` is receiving correct arguments - https://developer.mozilla.org/en-US/docs/Web/API/PasswordCredential – Aldis Jul 06 '23 at 11:23
0

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)
Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
aanders77
  • 620
  • 8
  • 22
  • 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 Jan 17 '14 at 20:08
  • I have updated the question after simplifying the issue greatly. – EleventyOne Jan 23 '14 at 03:28