Improve the login experience with the Credential Management API

 
 
  • Gérald Barré

Many websites require users to log in to access their resources. From a user point of view, the login process can be complicated, and this is more true when you can log in using a login/password or using a social provider (Microsoft, Google, Facebook, etc.). For instance, some users enter their Google credentials in the Username/Password form instead of clicking the Google button, or they don't remember which provider they use.

To help users, major web browsers allow saving credentials, and auto-fill forms. This allows users to quickly log into the web site. This is great but this doesn't work with social providers and you still need to navigate to the login page. Thanks to the new Credential Management API you can go further. Indeed, the browser knows your credentials, so why not automatically log you in as soon as you access the web site without even navigating to the login page? To be clear, users may see the login page only the first time. Then, he or she can log in without typing their credentials and without navigating to the login page.

#Can I use the Credential Management API?

This API is not well supported. Indeed, only Google Chrome and the developer version of Opera (44) support it. However, this doesn't mean you should not consider using it. Using this API doesn't break the default login flow. Instead, this API just improves it.

Source: https://caniuse.com/#feat=credential-management

#How it works?

For the demo, I'll use ASP.NET Core and TypeScript. The code is very basic, so it's very easy to adapt it to another language/framework.

First, you need to create a login form:

Razor
<form asp-controller="Account" asp-action="Login" method="post">
    <input asp-for="Email" />
    <input asp-for="Password" />
    <button type="submit">Log in</button>
</form>

Then, you must create a controller action. This code comes from the default template of ASP.NET with Individual User Accounts.

C#
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: false); // Create the authentication cookie
        if (result.Succeeded)
            return RedirectToLocal(returnUrl);
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }

    return View(model);
}

Now, when the user logs in, the browser should display a "Save credential" button:

Once the user has saved the password, you'll be able to access its credentials from JavaScript. For security reasons, you won't have access to the username or the password directly. If you use TypeScript, you need to add the type declarations:

Shell
npm install @types/webappsec-credential-management

The basic code to get a saved credential for the current website is:

TypeScript
async function signIn(unmediated: boolean) {
    // Test if the Credential Manager API exists
    if (navigator.credentials) {
        // Ask for a credential
        // unmediated: if true, the user agent will only attempt to provide a Credential without user interaction
        const cred = await navigator.credentials.get({
            password: true,
            unmediated: unmediated
        });

        if (cred) {
            // Do something with the creds
        }
    }
}

The API allows to manage username/password credentials and federated credentials (log in using an external provider such as Microsoft, Google, Facebook, etc.). You can determine the type using the type property. In this post, we'll only handle username/password credentials so we need to test if the type is password. As we use TypeScript we can create a type guard function so we can avoid casting explicitly the variable cred:

TypeScript
function isPasswordCredential(credential: Credential) : credential is PasswordCredential {
    return credential.type === "password";
}

As said before, you cannot directly access the username or the password of the credential. However, you can use the fetch function. This function can take a PasswordCredential as a parameter and send it to the server.

TypeScript
        if (cred) {
            if (isPasswordCredential(cred)) {
                cred.idName = "email"; // the username field is "email" as defined in the log in form (default "username")
                //cred.passwordName = "password";

                const response = await fetch("/Account/AutoLogin", {
                    method: "POST",
                    credentials: cred
                });

                if (response.ok) {
                    window.location.reload(); // reload the page with the authentication cookie
                }
            }
        }

If needed you can add additional data to the request. For instance, you can add the CSRF token:

TypeScript
                const additionalData = new FormData();
                const csrfInput = <HTMLInputElement>document.querySelector("input[name='__RequestVerificationToken']");
                additionalData.append("__RequestVerificationToken", csrfInput.value);
                cred.additionalData = additionalData;

                const response = await fetch("/Account/AutoLogin", {
                    method: "POST",
                    credentials: cred
                });

The AutoLogin action is the same as the Login action except it just returns a status code instead of an HTML content:

C#
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> AutoLogin(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, isPersistent: false);
        if (result.Succeeded)
            return Ok();

        return BadRequest("Invalid login attempt.");
    }

    // If we got this far, something failed, redisplay form
    return Ok();
}

Now the login code is written, you can call the signIn function on page load (if the user is not authenticated).

TypeScript
signIn(true);

The first time or if the user has saved more than one credential, you must show the UI:

TypeScript
signIn(false);

After the user logs off, you don't want to be able to log the user in automatically. To instruct the browser to require the mediation after the user logs off, you must call the requireUserMediation function. This does not delete the saved credential. Instead, the navigator.credentials.get won't return any credential without the user accept it explicitly (unmediated: false).

TypeScript
if (navigator.credentials) {
    const logOutElement = document.getElementById("LogOut");
    if (logOutElement) {
        logOutElement.addEventListener("click", e => navigator.credentials.requireUserMediation());
    }
}

#Conclusion

This new API is a great improvement for the user as it simplifies the login process. You can now automatically log the user in without they notice it. While this new API is not well supported (currently Chrome and Opera), you can already use it to progressively replace the old login form. This post does not cover all the methods of this API. For instance, we do not use the store method which may be required for a SPA application. If you want to go further, here are some additional resources:

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub