WebOTP API

Draft Community Group Report,

This version:
http://wicg.github.io/WebOTP
Test Suite:
https://github.com/web-platform-tests/wpt/tree/master/sms
Issue Tracking:
GitHub
Editor:
(Google Inc.)

Abstract

A Javascript API to request one time passwords for verifying credentials (e.g. phone numbers, emails).

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

logo

1. Introduction

This section is non-normative.

Many web sites need to verify credentials (e.g. phone numbers and email addresses) as part of their authentication flows. They currently rely on sending one-time-passwords (OTP) to these communication channels to be used as proof of ownership. The one-time-password is manually handed back by the user (typically by copying/pasting) to the web app which is onerous and erroneous.

This a proposal for a client side javascript API that enables web sites to request OTPs and a set of transport-specific conventions (we start with SMS while leaving the door open to others) that can be used in coordination with browsers.

1.1. The client side API

In this proposal, websites have the ability to call a browser API to request OTPs coming from specific transports (e.g. via SMS).

The browser intermediates the receipt of the SMS and the handing off to the calling website (typically asking for the user’s consent), so the API returns a promise asynchronously.

let {code, type} = await navigator.credentials.get({
  otp: {
    transport: ["sms"]
  }
});

1.2. The server side API

Once the client side API is called, the website’s server can send OTPs to the client via the requested transport mechanisms. For each of these transport mechanism, a server side convention is set in place to guarantee that the OTP is delivered safely and programatically.

For SMS, for example, servers should send origin-bound one-time code messages to clients. [sms-one-time-codes]

In the following origin-bound one-time code message, the host is "example.com", the code is "123456", and the explanatory text is "Your authentication code is 123456.\n".

"Your authentication code is 123456.

@example.com #123456"

1.3. Feature Detection

Not all user agents necessarily need to implement the WebOTP API at the exact same moment in time, so websites need a mechanism to detect if the API is available.

Websites can check for the presence of the OTPCredential global interface:

if (!window.OTPCredential) {
  // feature not available
  return;
}

1.4. Web Components

For the most part, OTP verification largely relies on:

We expect some of these frameworks to develop declarative versions of this API to facilitate the deployment of their customer’s existing code.

Web Component Polyfills
<script src="sms-sdk.js"></script>

<form>
  <input is="one-time-code" required />
  <input type="submit" />
</form>

And here is an example of how a framework could implement it using web components:

Web Component Polyfills
customElements.define("one-time-code",
  class extends HTMLInputElement {
    connectedCallback() {
      this.receive();
    }
    async receive() {
      let {code, type} = await navigator.credentials.get({
        otp: {
         transport: ["sms"]
        }
      });
      this.value = otp;
      this.form.submit();
    }
  }, {
    extends: "input"
});

1.5. Abort API

Many modern websites handle navigations on the client side. So, if a user navigates away from an OTP flow to another flow, the request needs to be cancelled so that the user isn’t bothered with a permission prompt that isn’t relevant anymore.

To facilitate that, an abort controller can be passed to abort the request:

const abort = new AbortController();

setTimeout(() => {
  // abort after two minutes
  abort.abort();
}, 2 * 60 * 1000);
  
let {code, type} = await navigator.credentials.get({
  signal: abort.signal,
  otp: {
    transport: ["sms"]
  }
});

2. Client Side API

Websites call navigator.credentials.get({otp:..., ...}) to retrieve an OTP.

The algorithm of navigator.credentials.get() looks through all of the interfaces that inherit from Credential in the Request a Credential abstract operation.

In that operation, it finds OTPCredential which inherits from Credential. It calls OTPCredential.[[CollectFromCredentialStore]]() to collect any credentials that should be available without user mediation, and if it does not find exactly one of those, it then calls OTPCredential.[[DiscoverFromExternalSource]]() to have the user select a credential source and fulfill the request.

Since this specification requires an authorization gesture to create OTP credentials, the OTPCredential.[[CollectFromCredentialStore]]() internal method inherits the default behavior of Credential.[[CollectFromCredentialStore]](), of returning an empty set.

It is then the responsibility of OTPCredential.[[DiscoverFromExternalSource]]() to provide an OTP.

2.1. The OTPCredential Interface

The OTPCredential interface extends Credential and contains the attributes that are returned to the caller when a new one time password is retrieved.

OTPCredential's interface object inherits Credential's implementation of [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors), and defines its own implementation of [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors).

[Exposed=Window, SecureContext]
interface OTPCredential : Credential {
    readonly attribute DOMString code;
};
id

This attribute is inherited from Credential

[[type]]

The OTPCredential interface object's [[type]] internal slot's value is the string "otp".

code, of type DOMString, readonly

The retrieved one time password.

2.1.1. The [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) Method

This method is called every time navigator.credentials.get({otp:..., ...}) and is responsible for returning an OTP when one is requested (i.e. when options.otp is passed).

This internal method accepts three arguments:

origin

This argument is the relevant settings object's origin, as determined by the calling get() implementation, i.e., CredentialsContainer's Request a Credential abstract operation.

options

This argument is a CredentialRequestOptions object whose options.otp member contains a OTPCredentialRequestOptions object specifying the desired attributes of the OTP to retrieve.

sameOriginWithAncestors

This argument is a Boolean value which is true if and only if the caller’s environment settings object is same-origin with its ancestors. It is false if caller is cross-origin.

Note: Invocation of this internal method indicates that it was allowed by permissions policy, which is evaluated at the [CREDENTIAL-MANAGEMENT-1] level. See § 2.5 Permissions Policy integration.

Note: This algorithm is synchronous: the Promise resolution/rejection is handled by navigator.credentials.get().

When this method is invoked, the user agent MUST execute the following algorithm:

  1. Assert: options.otp is present.

  2. Let options be the value of options.otp.

  3. Let callerOrigin be origin. If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.

  4. Let effectiveDomain be the callerOrigin’s effective domain. If effective domain is not a valid domain, then return a DOMException whose name is "SecurityError" and terminate this algorithm.

    Note: An effective domain may resolve to a host, which can be represented in various manners, such as domain, ipv4 address, ipv6 address, opaque host, or empty host. Only the domain format of host is allowed here. This is for simplification and also is in recognition of various issues with using direct IP address identification in concert with PKI-based security.

  5. If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.

  6. TODO(goto): figure out how to connect the dots here with the transport algorithms.

During the above process, the user agent SHOULD show some UI to the user to guide them in the process of sharing the OTP with the origin.

2.2. CredentialRequestOptions

To support obtaining OTPs via navigator.credentials.get(), this document extends the CredentialRequestOptions dictionary as follows:

partial dictionary CredentialRequestOptions {
    OTPCredentialRequestOptions otp;
};
otp, of type OTPCredentialRequestOptions

This OPTIONAL member is used to make WebOTP requests.

2.3. OTPCredentialRequestOptions

The OTPCredentialRequestOptions dictionary supplies navigator.credentials.get() with the data it needs to retrieve an OTP.

dictionary OTPCredentialRequestOptions {
  sequence<OTPCredentialTransportType> transport = [];
};
transport, of type sequence<OTPCredentialTransportType>, defaulting to []

This OPTIONAL member contains a hint as to how the server might receive the OTP. The values SHOULD be members of OTPCredentialTransportType but client platforms MUST ignore unknown values.

2.4. OTPCredentialTransportType

enum OTPCredentialTransportType {
    "sms",
};
User Agents may implement various transport mechanisms to allow the retrieval of OTPs. This enumeration defines hints as to how user agents may communicate with the transport mechanisms.
sms

Indicates that the OTP is expected to arrive via SMS.

2.5. Permissions Policy integration

This specification defines one policy-controlled feature identified by the feature-identifier token "otp-credentials". Its default allowlist is 'self'. [Permissions-Policy]

A Document's permissions policy determines whether any content in that document is allowed to successfully invoke the WebOTP API, i.e., via navigator.credentials.get({otp: { transport: ["sms"]}}). If disabled in any document, no content in the document will be allowed to use the foregoing methods: attempting to do so will return an error.

2.6. Using WebOTP within iframe elements

The WebOTP API is available in inner frames when the origins match but it’s disabled by default in cross-origin iframes. To override this default policy and indicate that a cross-origin iframe is allowed to invoke the WebOTP API's [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) method, specify the allow attribute on the iframe element and include the otp-credentialst feature-identifier token in the allow attribute’s value.

Relying Parties utilizing the WebOTP API in an embedded context should review § 4.4 Visibility Considerations for Embedded Usage regarding UI redressing and its possible mitigations.

3. Transports

We expect a variety of different transport mechanisms to enable OTPs to be received, most notably via SMS, email and hardware devices.

Each of these transport mechanisms will need their own conventions on how to provide OTPs to the browser.

In this draft, we leave the API surface to be extensible to any number of transports.

3.1. SMS

One of the most commonly used transport mechanisms for OTP is via SMS messages, allowing developers to verify phone numbers. They are typically sent embedded in an SMS message, which gets copied and pasted by users.

[sms-one-time-codes] defines origin-bound one-time code messages, a format for sending OTPs over SMS and associating them with origins.

4. Security

From a security perspective, there are two considerations with this API:

4.1. Availability

This API is only available on:

This API is also only available via https or localhost (for development purposes). We don’t entirely adopt the concept of trustworthy urls because it covers more schemes (e.g. data://123) than we would like to (our initial intuition is that (a) https and localhost covers most cases and (b) it needs to be clear to the user what public facing entity its sending the SMS).

4.2. Addressing

Each transport mechanism is responsible for guaranteeing that the browser has enough information to route the OTP appropriately to the intended origin.

For example, origin-bound one-time code messages explicitly identify the origin on which the OTP can be used.

The addressing scheme must be enforced by the agent to guarantee that it gets routed appropriately.

4.3. Tampering

There isn’t any built-in cryptographic guarantee that the OTP that is being handed back by this API hasn’t been tampered with. For example, an attacker could send an origin-bound one-time code message to the user’s phone with an arbitrary origin which the agent happilly passes back to the requesting call.

Your verification code is: MUAHAHAHA

@example.com #MUAHAHAHA

It is the responsibility for the caller to:

4.4. Visibility Considerations for Embedded Usage

Simplistic use of WebOTP in an embedded context, e.g., within iframes as described in § 2.6 Using WebOTP within iframe elements, may make users vulnerable to UI Redressing attacks, also known as "Clickjacking". This is where an attacker overlays their own UI on top of a Relying Party's intended UI and attempts to trick the user into performing unintended actions with the Relying Party. For example, using these techniques, an attacker might be able to trick users into purchasing items, transferring money, etc.

5. Privacy

From a privacy perspective, the most notable consideration is for a user agent to enforce the consensual exchange of information between the user and the website.

Specifically, this API allows the programatic verification of personally identifiable attributes of the user, for example email addresses and phone numbers.

The attack vector that is most frequently raised is a targeted attack: websites trying to find a very specific user accross all of its user base. In this attack, if left unattended, a website can use this API to try to find a specific user that owns a specific phone number by sending all / some (depending on the confidence level) of its users an origin-bound one-time code message and detecting when one is received.

Notably, this API doesn’t help with the acquisition of the personal information, but rather with its verification. That is, this API helps verifying whether the user owns a specific phone number, but doesn’t help acquiring the phone number in the first place (it assumes that the website already has access to it).

Nonetheless, the verification of the possession of these attributes is extra information about the user and should be handled responsibly by a user agent, typically via permission prompts before handing back the OTP to the website.

6. Acknowledgements

Many thanks to Steven Soneff, Ayu Ishii, Reilly Grant, Eiji Kitamura, Alex Russell, Owen Campbell-Moore, Joshua Bell, Ricky Mondello and Mike West for helping craft this proposal.

Special thanks to Tab Atkins, Jr. for creating and maintaining Bikeshed, the specification authoring tool used to create this document, and for his general authoring advice.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Conformant Algorithms

Requirements phrased in the imperative as part of algorithms (such as "strip any leading space characters" or "return false and abort these steps") are to be interpreted with the meaning of the key word ("must", "should", "may", etc) used in introducing the algorithm.

Conformance requirements phrased as algorithms or specific steps can be implemented in any manner, so long as the end result is equivalent. In particular, the algorithms defined in this specification are intended to be easy to understand and are not intended to be performant. Implementers are encouraged to optimize.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CREDENTIAL-MANAGEMENT-1]
Mike West. Credential Management Level 1. 17 January 2019. WD. URL: https://www.w3.org/TR/credential-management-1/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[Permissions-Policy]
Ian Clelland. Permissions Policy. 16 July 2020. WD. URL: https://www.w3.org/TR/permissions-policy-1/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SMS-ONE-TIME-CODES]
Origin-bound one-time codes delivered via SMS. cg-draft. URL: https://wicg.github.io/sms-one-time-codes/
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/

IDL Index

[Exposed=Window, SecureContext]
interface OTPCredential : Credential {
    readonly attribute DOMString code;
};

partial dictionary CredentialRequestOptions {
    OTPCredentialRequestOptions otp;
};

dictionary OTPCredentialRequestOptions {
  sequence<OTPCredentialTransportType> transport = [];
};

enum OTPCredentialTransportType {
    "sms",
};