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's perspective, the login process can be complicated, and it becomes even more complex when there are multiple authentication options: username/password or 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 cannot remember which provider they used to create their account.
Sample login page
To help users, major web browsers allow saving credentials and auto-filling forms. This lets users log into a website quickly. However, this does not work with social providers and still requires navigating to the login page. The Credential Management API goes further: the browser already knows your credentials, so it can automatically sign you in as soon as you visit the website, without ever navigating to the login page. Users may only need to see the login page the first time. After that, they can sign in without typing their credentials or navigating to the login page.
#Can I use the Credential Management API?
This API has limited browser support: only Google Chrome and Opera support it. However, that does not mean you should avoid using it. Chrome is the most-used browser and, according to Can I Use, represents about 67% of all users. Additionally, using this API does not break the default login flow in other browsers; it only enhances it when available.
Credential Management API - Support (source))
#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:
HTML
<form method="post">
<input type="email" name="Email" />
<input type="password" name="Password" />
<button type="submit">Log in</button>
</form>
Then, you need to create a controller action. This code is taken from the default ASP.NET template 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 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 their password, you can access their credentials from JavaScript. For security reasons, you only have access to credentials for the current domain. If you use TypeScript, add the type declarations before using the new API:
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) {
// 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 supports both username/password credentials and federated credentials (signing in via an external provider such as Microsoft, Google, or Facebook). You can determine the credential type using the type property. In this post, we only handle username/password credentials, so we check whether the type is password. Since we use TypeScript, we can create a type guard function to avoid explicitly casting the cred variable:
TypeScript
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, then create a FormData object and send it to the server.
TypeScript
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:
TypeScript
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 identical to the Login action except it returns a status code instead of 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.");
}
return BadRequest();
}
Now that the login code is in place, call the signIn function on page load if the user is not authenticated.
TypeScript
signIn(false);
Authentication using Credential Management API
After the user logs off, you should not automatically sign them back in without their consent. To require browser mediation after logout, call the preventSilentAccess function. This does not delete the saved credential; it forces the browser to show a prompt the next time you call navigator.credentials.get. This way, the user is aware of the authentication attempt and can cancel it if needed.
TypeScript
if (navigator.credentials) {
const logOutElement = document.getElementById("LogOut");
if (logOutElement) {
logOutElement.addEventListener("click", e => navigator.credentials.preventSilentAccess());
}
}
#Conclusion
The Credential Management API is a great improvement for users as it simplifies the login process. You can automatically sign users in without them noticing. While browser support is currently limited to Chrome and Opera, you can already use it to progressively enhance your login flow. This post does not cover all methods of this API; for instance, it does not address the store method, which may be required for SPA applications. If you want to learn more, here are some additional resources:
Do you have a question or a suggestion about this post? Contact me!