Automatically log in a user on a website using the Credential Management API?

  • .NET
  • Security
  • Web

This post is part of the series 'Password management'. Be sure to check out the rest of the blog posts of the series!

Many websites require users to log in to access their resources. From a user point of view, the login process can be complicated, and it's even more complex when there are multiple ways to authenticate: 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 have used to create their account.

Sample login page

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, they can log in without typing their credentials and without navigating to the login page.

Can I use the Credential Management API?

This API is clearly not well supported. Indeed, only Google Chrome and Opera support it. However, this doesn't mean you should not consider using it. First, Chrome is the most used browser. If you look at CanIUse, it represents about 67% of all users. Plus, using this API doesn't break default login flow on other browsers. Instead, this API just improves it when it is available.

Credential Management API - Support

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

How does it work?

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 server and client language/framework.

First, you need to create a login form:

<form method="post">
    <input type="email" name="Email" />
    <input type="password" name="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, nothing fancy here.

[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 the email and password are valid
        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:

Google Chrome - Save credential

Once the user has saved the password, you'll be able to access its credentials from JavaScript. For security reason, you will only have access to the credentials of the current domain. If you use TypeScript, you need to add the type declarations before using the new API:

npm install @types/webappsec-credential-management

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

async function signIn(unmediated: boolean) {
    // Test if the Credential Manager API exists
    if (navigator.credentials) {
        // Get the saved 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 managing 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:

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

Once you have the credential object, you can access the id (username) and password properties. So, you can create a FormData and send it to the server.

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

        if (cred) {
            if (isPasswordCredential(cred)) {
                let form = new FormData();
                form.append('email', cred.id);
                form.append('password', cred.password);
                const response = await fetch('/Account/AutoLogin', {
                    method: 'POST',
                    credentials: 'include',
                    body: form
                });

                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:

        if (cred) {
            if (isPasswordCredential(cred)) {
                let form = new FormData();
                form.append('email', cred.id);
                form.append('password', cred.password);

                // Get the value of the anti CSRF field generated by ASP.NET Core
                const csrfInput = <HTMLInputElement>document.querySelector("input[name='__RequestVerificationToken']");
                additionalData.append("__RequestVerificationToken", csrfInput.value);

                const response = await fetch('/Account/AutoLogin', {
                    method: 'POST',
                    credentials: 'include',
                    body: form
                });

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

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

[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.");
    }

    return BadRequest();
}

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

signIn(false);

Authentication using Credential Management API

After the user logs off, you don't want to be able to log the user in automatically without their consent. To instruct the browser to require the mediation after the user logs off, you must call the preventSilentAccess function. This does not delete the saved credential. It just forces the browser to show the UI the next time you call navigator.credentials.get. This means the user will notice you want to authenticate they and they can cancel the authentication if they want to.

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

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 log in 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're some additional resources:

Follow me:
Enjoy this blog?Buy Me A CoffeeDonate with PayPal

Leave a reply