├── .nojekyll ├── CONTRIBUTING.md ├── FAQ.md ├── LICENSE.md ├── README.md ├── explainer.md ├── index.bs ├── index.html ├── logo-otp.png ├── logo-otp.svg └── w3c.json /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WICG/web-otp/d9efe8da6035690a513b5293e16669e2f436f675/.nojekyll -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License 4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions, 5 | you must join the CG. 6 | 7 | If you are not the sole contributor to a contribution (pull request), please identify all 8 | contributors in the pull request comment. 9 | 10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 11 | 12 | ``` 13 | +@github_username 14 | ``` 15 | 16 | If you added a contributor by mistake, you can remove them in a comment with: 17 | 18 | ``` 19 | -@github_username 20 | ``` 21 | 22 | If you are making a pull request on behalf of someone else but you had no part in designing the 23 | feature, you can remove yourself with the above syntax. 24 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | ## Frequently Asked Questions 3 | 4 | [TOC] 5 | 6 | ### Why are we telling developers to use phone numbers? They can be hijacked, change ownership, used to track users, etc. 7 | 8 | We aren’t telling developers to use phone number; they are already using them. Despite problems, phone numbers are useful for various purposes (see intro). In the absence of alternatives to achieve these useful things, developers will continue to use phone numbers and users will struggle with them. This proposal makes the web more usable in the short term (in particular matching functionality that already existings natively), but in the long run, we hope to move the ecosystem off of phone numbers as alternatives for the need functionality become available. Given the time-scale of such a transition, action in the short-term to reach experience parity seems necessary. 9 | 10 | ### Why is this using SMS OTP as a verification mechanism? SMS OTP are slow, expensive, phishable, etc. 11 | 12 | SMS is the most common existing verification mechanism; this proposal aims to streamline flows that already exist by mitigating the need for user involvement/action. Although this does not address the more fundamental problems, it may make some situations better (e.g. users don’t handle OTP manually, hence less conditioned to be phished), but starting to move developers to a programmatic model would be a potential stepping stone to better mechanisms. 13 | 14 | ### Is this implementable on iOS? 15 | 16 | Yes, iOS already uses a declarative model (form annotation “[one-time-code](https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element)”) and heuristically extracts and suggests OTP from SMS. They could implement an imperative API or support other alternatives like facilitating interaction with identity providers if desired. 17 | 18 | ### Is this implementable on desktop? 19 | 20 | Yes, browser need not look for SMS on the local device. For example, if Chromesync is enabled across desktop and mobile devices, Chrome could retrieve on one device and return the SMS content on another. Or locally, a desktop instance of a browser could use Bluetooth to talk to a nearby phone and have an instance of the browser on the mobile device retrieve and return the SMS. 21 | 22 | ### Why can’t we use a model like having developer tell us the OTP and let them know if there is a match? 23 | 24 | The developer needs something that can be verified on their server, a yes/no response is not sufficient. Claiming that the phone number is active on the device would require a response signed in some way to make it verifiable. This could be done in a number of ways, but is a major departure from the current OTP model and necessitate major changes from developers (i.e. change their backend server logic to process these claims; whereas just returning the SMS contents with the OTP means primarily only a frontend change an minimal backend work ... perhaps only changing the message template format, rather than security-sensitive backend logic). However, the longer-term intent with more comprehensive Identity APIs is to provide verifiable assertions of phone ownership, which would be compelling for developers to adopt and change their system to accept if it meant avoiding the need for sending SMS. 25 | 26 | ## Self-Review Questionnaire: Security and Privacy 27 | 28 | https://www.w3.org/TR/security-privacy-questionnaire 29 | 30 | ### What information might this feature expose to Web sites or other parties, and for what purposes is that exposure necessary? 31 | 32 | The feature **facilitates** the **verification** of phone numbers on the web. Notably: 33 | 34 | * it does not **capacitate** the verification: phone number verification via OTPs is already largely deployed in an already capable web (i.e. input boxes and copy/paste). 35 | * it does not facilitate the **acquisition** of phone numbers on the web (e.g. ``). 36 | 37 | In that context, it is a feature that decreases the friction of verifying phone numbers programmatically with the proper user consent. 38 | 39 | We believe the feature is valuable because it supports businesses achieving higher conversation rates in acquisition funnels (which start primarily through ads), moving the needle to the bottom line of their economic sustainability, decreasing friction for users while keeping their consent. 40 | 41 | ### Is this specification exposing the minimum amount of information necessary to power the feature? 42 | 43 | We believe so. It currently exposes an OTP value (an alphanumeric number) that is generated by the website, which confirms the user's possession of the telephone number. 44 | 45 | ### How does this specification deal with personal information or personally-identifiable information or information derived thereof? 46 | 47 | OTPs are ephemeral one time passwords that are generated programmatically and stored in servers, meaning that they expire quickly and can't be used to identify a specific individual effectively in case they are intercepted. They aren't global identifiers either, so they can't be used in server side joins. 48 | 49 | ### How does this specification deal with sensitive information? 50 | 51 | The most sensitive information that is being shared is the confirmation that the user is in possession of a specific phone number (hence, accomplishing the goal of phone number verification). 52 | 53 | For that reason, user agents should mediate that consent appropriately, typically via permission prompts. 54 | 55 | ### Does this specification introduce new state for an origin that persists across browsing sessions? 56 | 57 | No. OTPs are ephemeral and handed back, but not stored. 58 | 59 | ### What information from the underlying platform, e.g. configuration data, is exposed by this specification to an origin? 60 | 61 | This API doesn't expose any direct affordance available in the underlying platform. The user has to explicit consent the sharing of the OTP with the origin, so the origin cannot infer any information from a failed request (e.g. whether the user is on a cellular network or not, whether the user is on a phone or not). 62 | 63 | ### Does this specification allow an origin access to sensors on a user’s device 64 | 65 | No. 66 | 67 | ### What data does this specification expose to an origin? Please also document what data is identical to data exposed by other features, in the same or different contexts. 68 | 69 | This feature doesn't cross any origin boundary. 70 | 71 | ### Does this specification enable new script execution/loading mechanisms? 72 | 73 | No. 74 | 75 | ### Does this specification allow an origin to access other devices? 76 | 77 | No. 78 | 79 | There is a possible implementation where devices without access to the cellular network (e.g. desktop devices) could coordinate with devices with access to the cellular network (e.g. phones), but we haven't gotten that far yet. 80 | 81 | ### Does this specification allow an origin some measure of control over a user agent’s native UI? 82 | 83 | Yes. 84 | 85 | The browser intermediates the user consent flow using data passed by the origin, specifically, the OTP value (which is an alpha numeric value) but more generally the contents of the SMS. 86 | 87 | We went through a series of UX explorations and a series of reviews with developers and users to find one that we were confident about. 88 | 89 | ### What temporary identifiers might this this specification create or expose to the web? 90 | 91 | None that we are aware of. 92 | 93 | One could call an OTP an identifier, but it is (i) short lived, (ii) rotated and (iii) limited to the origin's purview, so it can't be joined. 94 | 95 | ### How does this specification distinguish between behavior in first-party and third-party contexts? 96 | 97 | This feature isn't available in third party contexts. 98 | 99 | ### How does this specification work in the context of a user agent’s Private Browsing or "incognito" mode? 100 | 101 | This features doesn't make any distinction between "incognito" and non "incognito" mode. 102 | 103 | ### Does this specification have a "Security Considerations" and "Privacy Considerations" section? 104 | 105 | [Yes]([https://wicg.github.io/WebOTP/#security](https://wicg.github.io/WebOTP/#security)) and [yes]([https://wicg.github.io/WebOTP/#privacy](https://wicg.github.io/WebOTP/#privacy)). 106 | 107 | ### Does this specification allow downgrading default security characteristics? 108 | 109 | No. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors 2 | under the 3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). 4 | 5 | Contributions to Specifications are made under the 6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 7 | 8 | Contributions to Test Suites are made under the 9 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html) 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebOTP 2 | 3 | This repository documents an API for accessing one time passwords to verify credentials (e.g. phone numbers). 4 | 5 | * The [explainer](explainer.md) is a developer-oriented preview of the API. 6 | * The [specification draft](https://wicg.github.io//WebOTP/index.html) is aimed at browser developers. 7 | 8 | The API has a [test suite](https://github.com/web-platform-tests/wpt/tree/master/sms) in the Web Platform Test project. 9 | 10 | * Example usage: https://web.dev/web-otp/ 11 | * Cross-browser OTP form best practices: https://web.dev/sms-otp-form/ 12 | 13 | # Resources 14 | 15 | This API is inspired by and loosely based on the following discussions: 16 | 17 | * [SMS Verification on Android](https://developers.google.com/identity/sms-retriever/overview) 18 | * [The one-time-code autocomplete API](https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element) 19 | 20 | The WebOTP API has also been discussed in the following places: 21 | 22 | * [Discourse thread](https://discourse.wicg.io/t/sms-otp-retrieval/3499) 23 | * Blink [intent-to-implement](https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/Drmmb_t4eE8/khjMpM9qBAAJ) 24 | * Blink [intent-to-experiment](https://groups.google.com/a/chromium.org/d/msg/blink-dev/-bdqHhCyBwM/yFoKtQQRAQAJ) 25 | * [TAG Review](https://github.com/w3ctag/design-reviews/issues/391) 26 | * [Origin Trials](https://web.dev/sms-receiver-api-announcement/) 27 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | 2 | # WebOTP 3 | 4 | > **WARNING**: The UI and mechanism are outdated and we are updating the content. See https://web.dev/web-otp/ for how the API looks today. 5 | ## TL;DR; 6 | 7 | Many web sites rely on verifying phone numbers via sending one-time-passwords (OTP) via SMS, which instructs users to copy/paste it into the website. 8 | 9 | This a proposal for (a) a client side javascript API that enables web sites to request OTPs and (b) a server side formatting convention that enables browsers to route SMSes to them. Here is what the client side API looks like: 10 | 11 | ```javascript 12 | let {code, type} = await navigator.credentials.get({otp: {transport: ["sms"]}}); 13 | // code == "123ABC78" 14 | // type == "otp" 15 | ``` 16 | 17 | And here is the server side formatting convention: 18 | 19 | ``` 20 | Your verification code is: 123ABC78 21 | 22 | @example.com #123ABC78 23 | ``` 24 | 25 | ## Introduction 26 | 27 | Developers use phone numbers for many aspects of building an application: 28 | 29 | * account identifier (especially in emerging markets where email usage is low) 30 | * social graph (based on phone number contact list for example) 31 | * communication channel (i.e. call the user, send text message, etc.)u 32 | * anti-abuse signal (phone numbers are limited, may require physical identity verification in some places) 33 | * multi-factor auth (e.g. 2-step verification with SMS OTP) 34 | * account recovery (e.g. look up a forgotten account, as a password reset option) 35 | 36 | The challenge is that usage of phone numbers for these purposes typically requires proof that a user currently controls the phone number (phone number verification), and existing verification mechanisms on the web are cumbersome, requiring users to manually input one-time verification codes. Easing this has been a long standing feature request for the web from many of the largest global developers. 37 | 38 | There are a variety of ways to verify control over phone numbers, but a randomly generated one-time passcode (OTP) sent by SMS to the number in question is the most common. Presenting this code back to developer’s server proves control of the phone number. In this proposal, we focus on the ability to programmatically obtain one-time codes from SMS as solution to ease the friction and failure points of manual user input of SMS codes, which is prone to error and phishing. 39 | 40 | * goal: make the most [common existing](#scenarios) phone number verification flow (SMS OTP) match ease of use in native apps. 41 | * non-goal: this proposal does not attempt to move developers off of existing phone number use cases and verification mechanisms, nor does it cover obtaining the phone number itself. Specifically, the developer must have previously obtained the phone number via existing mechanisms (e.g. user form input, autofill, etc.), which aren't addressed in this proposal. 42 | 43 | ## Prior Art 44 | 45 | There are two comparable APIs that we should use as a reference. 46 | 47 | First, the native [Android API](https://developers.google.com/identity/sms-retriever/overview) is an **imperative** API that gives access to the **full** contents of the SMS message. Here is what it looks like: 48 | 49 | ```java 50 | // Starts SmsRetriever, which waits for ONE matching SMS message until timeout 51 | // (5 minutes). The matching SMS message will be sent via a Broadcast Intent with 52 | // action SmsRetriever#SMS_RETRIEVED_ACTION. 53 | SmsRetrieverClient client = SmsRetriever.getClient(this /* context */); 54 | Task task = client.startSmsRetriever(); 55 | ``` 56 | 57 | In order to use the native SMS retrieval mechanism, the SMS message content must be formatted appropriately, with a hashcode derived from the native app package and cert fingerprint. For example: 58 | 59 | ``` 60 | <#> Your ExampleApp verification code is: 123ABC78 61 | FA+9qCX9VSu 62 | ``` 63 | 64 | Secondly, Safari on iOS has a declarative [autocomplete](https://developer.apple.com/documentation/security/password_autofill/enabling_password_autofill_on_an_html_input_element) API that provides an integration with the native keyboard. iOS applies heuristics to extract OTPs from SMSes to pass it back to the `` element. Here is what it looks like: 65 | 66 | ```html 67 | 68 | ``` 69 | 70 | ## Proposal 71 | 72 | The following is an early exploration / baseline of what this API could look like. We expect them to change drastically as we learn more about the space. 73 | 74 | From a UX perspective, we want to get out of the way as much as possible from the web author, while still keeping users aware and in control of what's going on. 75 | 76 | 77 | 78 | To support this user flow, we propose two complementary API components: 79 | 80 | * a client-side [javascript API](#imperative-api) 81 | * a [formating convention](#formatting) for SMS messages 82 | 83 | The former gives web pages a mechanism to receive SMSes and the latter is used as a mechanism to make sure that the origin boundaries are kept without additional mediation / gesture from the user. 84 | 85 | ## API Alternatives Considered 86 | 87 | There are two big formulations for this API that are more or less isomorphic to what's found on Android and iOS (see [prior art](#prior-art)). 88 | 89 | As we evaluate these, it is critical that we agree on a consistent criteria to measure them. From a high level perspective, we used the following ordering of constituents: 90 | 91 | 1. **users** first 92 | 2. **developers** second 93 | 3. **browser** engineers third 94 | 4. technical **purity** last 95 | 96 | With users as our highest order constituent, here are some of the criteria we used to evaluate [alternatives](#alternatives-considered): 97 | 98 | - privacy: to what extent does it secure the user's data? 99 | - efficiency: does it increase conversion rates? decrease number of taps? 100 | - complexity: does it introduce new / unnecessary concepts? 101 | - efficacy: does it cover all corner cases? 102 | 103 | With that, before we go any further, the following are some of the UX alternatives we considered. 104 | 105 | It is unclear if these formulations are necessarily mutually exclusive, but our intuition at the moment is that some of them are not. 106 | 107 | Before we get into API design, from a UX perspective, our initial intuition was that, under the right circumstances (e.g. the PWA is installed), the following UX formulation would lead to the highest levels of conversion rates: 108 | 109 | 110 | 111 | Having said that, here are other alternatives we considered. 112 | 113 | ### Alternatives Considered 114 | 115 | There are many different UX formulations that are under consideration, with different trade-offs between user awareness, control and friction. Here are the ones worth exploring further: 116 | 117 | ##### Automatic UX 118 | 119 | The automatic UX formulation ports the user experience you'd find on Android to the Web. 120 | 121 | In this formulation, all of the UX is delegated to the developer: status indicators, intent of the flow, etc. Whenever a browser implementation may find appropriate (e.g. when the PWA is installed, possibly), a browser may chose to share the SMS directly with the developer without any user consent. 122 | 123 | Some positives here: 124 | 125 | * no user friction / taps necessary 126 | * no concept of OTP introduced 127 | 128 | And the biggest challenge being: 129 | 130 | * does it sufficiently protect the user's privacy? 131 | 132 | 133 | 134 | ##### Opt-Out UX 135 | 136 | A variation of the previous UX that makes sure that the user is aware of what's going on, but still offers zero-tap conversion rates, is what we have been calling an opt-out UI: 137 | 138 | 139 | 140 | ##### Unblocking UX 141 | 142 | On occasions where the user agent believes it is necessary to mediate / regulate (e.g. there isn't enough signals of trust established between the user and the origin) and get user consent before sharing the SMS, something like an InfoBar could be used: 143 | 144 | 145 | 146 | ##### Opt-In UX 147 | 148 | A more abrupt / opinionated variation of the latter UX is to use a modal dialog: 149 | 150 | 151 | 152 | ##### Autofill UX 153 | 154 | At the end of the spectrum, the most regulated / mediated UX is the autofill UX, which makes sure that the intent to share is well established by making a suggestion with a keyboard accessory: 155 | 156 | 157 | 158 | Now that we established some **users** first UX formulations, lets look into our second constituents, **developers**. 159 | 160 | ### Declarative API 161 | 162 | The easiest starting point to enable this API is a declarative autocomplete field: 163 | 164 | ```html 165 | 166 | ``` 167 | 168 | This has well established distribution and an existing implementation by Safari. It is ergonomically simple (easy to adopt) and effective (reuses the privacy properties of autofill suggestions). 169 | 170 | While this doesn't seem at first **mutually exclusive** (and may be, in effect, collectively constructive), to close the gap with the [tap-less android user experience](#prior-art), the declarative API ties ourselves to: 171 | 172 | * form elements 173 | * the autofill permission model 174 | 175 | So, working backwards from where we believe we want to be, the declarative autofill API wouldn't allow us to fully close the gap with the kind of experience that you'd find on [native apps](#automatic-ux). 176 | 177 | ### Imperative API 178 | 179 | In this formulation, browsers provide an imperative API to request the contents of an incoming SMS. Here is one possible formulation / shape, based on Android’s [SMS Retriever API](https://developers.google.com/identity/sms-retriever/overview): 180 | 181 | ```javascript 182 | if (window.OTPCredential) { 183 | alert("feature not available :("); 184 | return; 185 | } 186 | try { 187 | let {code} = await navigator.credentials.get({otp: {transport: ["sms"]}); 188 | alert("otp received! " + code); 189 | } catch (e) { 190 | alert("timed out!"); 191 | } 192 | ``` 193 | 194 | There are a couple of nice side effects of the imperative API. 195 | 196 | First, it can offer browser engines a spectrum of mediation / consent / permissions / interventions without any re-activation of the ecosystem (e.g. a [permission-less UX](#automatic-ux), a [non-blocking UX](#unblocking-ux) and an [autofill UX](#autofill-ux)) 197 | 198 | Second, it can derive the [declarative API](#Declarative-API) (the opposite isn't true). An interesting implication of uncovering the lower level imperative API is that it can derive the high level declarative API without any loss of (a) browser mediation and (b) graceful degradation. 199 | 200 | Here is an example of a custom element that can be embedded in pages to polyfill existing deployments of the declarative autofill API: 201 | 202 | ```javascript 203 | /** 204 | * one-time-code is a custom element that extends elements 205 | * with an autocomplete="one-time-code" with the imperative 206 | * navigator.credentials.get({otp: {transport: ["sms"]}) API. 207 | * Submits the form when it receives the SMS. 208 | * 209 | * Example: 210 | * 211 | *
212 | * 213 | * 214 | *
215 | * 216 | * Degrades gracefully to the autofill UI or manual input when the 217 | * API is not available. 218 | * 219 | */ 220 | customElements.define("one-time-code", 221 | class extends HTMLInputElement { 222 | connectedCallback() { 223 | this.receive(); 224 | } 225 | async receive() { 226 | try { 227 | let {code} = await navigator.credentials.get({otp: {transport: ["sms"]}}); 228 | this.value = code; 229 | this.form.submit(); 230 | } catch (e) { 231 | console.log(e); 232 | } 233 | } 234 | }, { 235 | extends: "input" 236 | }); 237 | ``` 238 | 239 | The main drawback of this formulation is that it is (deliberately) unaware of the `` it is dealing with, so it can't necessarily be smart about it (e.g. suppress the virtual keyboard when the SMS arrives). 240 | 241 | Arguably, the right long term answer is to allow custom elements to have greater controls while [participating in forms]([https://github.com/mozilla/standards-positions/issues/150](https://github.com/mozilla/standards-positions/issues/150)) but that is still an area of active research. 242 | 243 | ### Formatting 244 | 245 | In this proposal, to support the isolation between different origins (without extra user mediation), we define a formatting convention in the SMS message that enables them to be addressed to a specific origin and routed by the browser securely: 246 | 247 | ``` 248 | Your OTP is: 123ABC78. 249 | @example.com #123ABC78 250 | ``` 251 | 252 | Long term, we expect the formatting to be browser agnostic ([current formulation](https://github.com/samuelgoto/sms-receiver/issues/4#issuecomment-528991114) of the long term plan), but while GMS core releases are still rolling out, Android still needs an [app hash](https://developers.google.com/identity/sms-retriever/verify#computing_your_apps_hash_string) to know which APK it should redirect the SMS to. There is an interesting trick we could do to combine URLs with App Hashes, embedding them as URL parameters (making them valid android SMSes as well as valid web urls, which we can use to derive origins): 253 | 254 | ``` 255 | Your OTP is: 123ABC78. 256 | @code.sgo.to #123ABC78 ^s3LhKBB0M33 257 | ``` 258 | 259 | In this formulation, the last few characters (e.g. `^s3LhKBB0M33`) are used to route the SMS from Android to the Browser APK and the origin is used to route from the Browser process to the right requesting tab. 260 | 261 | ## Security 262 | 263 | From a security perspective, the biggest consideration with this API is crossing an origin boundary, which we believe is mitigated by the [formatting](#formatting) addressing scheme. 264 | 265 | This API is also **only** available via `https` or `localhost` (for development purposes). We don't entirely adopt the concept of [trustworthy urls](https://www.w3.org/TR/powerful-features/#is-url-trustworthy) 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). 266 | 267 | This API is also **only** available on main frames, to avoid abuse by third party iframes / libraries. 268 | 269 | ## Privacy 270 | 271 | From a privacy perspective, there are a few considerations to be taken: 272 | 273 | - inner frames and ad networks 274 | - fingerprinting 275 | - awareness and control 276 | 277 | The first concern is somewhat easy to address: we propose the API should be **unavailable outside of top level frames**. 278 | 279 | The second and the third concerns are hard to be talked about abstractly, outside of a specific [UX formulation](#UX). We believe, however, that under the proposed UX formulation, the following attack vectors are addressed. 280 | 281 | ### User Tracking 282 | 283 | Phone numbers are an effective stable identifier for a user that enable cross-site and online/offline tracking. Obtaining the phone number is the point at which user is typically (or should be) asked for consent and best educated about the implications of sharing this information, so is not addressed in this proposal. 284 | 285 | However, the ability to verify the user’s phone number automatically in the background via an SMS retrieval API is a mechanism by which ongoing presence of the user could be determined, at least on a particular device where the developer already knows the phone number. Existing Android APIs mitigate this by allowing existing SMS notifications and SMS history to continue to be visible to the user, giving them insight that this may be happening (and providing a disincentive for a service to “guess” the user’s phone number, spam the number, and try to detect if this user is present by seeing if retrieval works). In practice, in the Android native app ecosystem, this hasn’t been found this to be a vector for abuse, especially given the cost of sending SMS and the visibility of the attack to users. 286 | 287 | ### Phishing 288 | 289 | SMS OTP are readily phishable and an existing widespread concern. While not making this worse, this proposal attempts to mitigate by avoiding and lessening the occurrence of users to manually enter OTP (so as to be less conditioned to phishing, and/or more conscious of where they enter OTP), and by making the OTP only available via programmatic mechanism to the intended recipient (i.e. by specifying the target origin in the message contents). 290 | 291 | ## Annex 292 | 293 | ### Related APIs 294 | 295 | #### Credential Management and WebAuthn APIs 296 | 297 | CM API and WebAuthn facilitate alternative forms of authentication. CM API facilitates interaction with password credentials and WebAuthn allows developers to interact with authentication hardware that provides much stronger, phishing resistant, and more usable multi-factor authentication on the web. While better alternatives for authentication, these APIs do not provide any communication mechanism or reputation signals that developers also use phone numbers for, so are not a comprehensive alternative to phone number or SMS OTP. 298 | 299 | #### Notifications 300 | 301 | Browser notification APIs provide a communication channel to developers, but developers often still prefer or also request a verified phone numbers since it checks that the user is reachable at this number and facilitates voice communication and reasonably real-time two-way communication on practically any time of mobile phone (no dependence on OS or version, pretty much all phones can handle SMS). 302 | 303 | #### OAuth etc. 304 | 305 | OAuth and similar protocols allow developers to obtain information such as verified phone number from an identity provider (IDP). However, this relies on user having provided, verified, and maintain their phone number with the IDP, and be comfortable using this model to share data (e.g. knowing their usage of 3p services). The IDPs themselves (unless an authoritative provider for a phone number, such as a carrier), need to verify phone number ownership, and typically still use SMS OTP for that purpose. 306 | 307 | #### reCAPTCHA 308 | 309 | Captcha APIs provide an alternative anti-abuse signals (i.e. that user is not a bot) that developer sometime rely on phone numbers for (in that phone numbers are often limited and require human involvement to procure, and as such as hard to produce at scale). 310 | 311 | #### Payment APIs 312 | 313 | Phone numbers are sometimes used for carrier billing schemes. Payment APIs offer an alternative as well as signal of user quality (having a payment instrument often involves identity verification and ability / history of being able to pay). 314 | 315 | 316 | #### Heuristic Autofill 317 | 318 | In addition to autofill annotations, the browser could also heuristically extract and autofill OTP, with user confirmation and without explicit developer support. 319 | 320 | However, getting access to SMS without coordination from the developer (i.e. without explicit formatting of the SMS or indication that developer is expecting an SMS) will be a challenge, as browser would require ongoing access to SMS. 321 | 322 | Note that iOS provides heuristic-based OTP autofill, but iOS provides browser / keyboard access to SMS; but on Android, browsers may not have or even be able to request indefinite SMS access. 323 | 324 | #### Phone Number Assertion API 325 | 326 | If phone number has already been verified for a given device or user account, browser could return a verifiable assertion of the phone number. 327 | 328 | ``` 329 | // This is just a draft/example of what a API could look like. 330 | let phone = await navigator.credentials.get({phone: true}); 331 | verify(phone); 332 | ``` 333 | 334 | This could be implemented by having developer interact with identity providers (IDPs), which have already verified and are aware of the user’s phone number, and could vouch for this information, in the same way Google Sign-In and similar federated identity flows currently work for email addresses. 335 | 336 | Several identity providers such as Facebook (Account Kit), Truecaller, and others already provide APIs like this verified phone numbers. 337 | 338 | ### Spec 339 | 340 | There are a couple of alternatives that we considered from an API shape perspective. First, subclassing `EventTarget`: 341 | 342 | ```javascript 343 | let receiver = new OTPReceiver(); 344 | receiver.addEventListener("receive", ({content}) => { 345 | console.log(content); 346 | }); 347 | receiver.receive(); 348 | ``` 349 | 350 | But it seemed awkward since this is a one-shot event. 351 | 352 | The other formulation that we think is worth noting is to use a static method outside of `navigator`. Example: 353 | 354 | ```javascript 355 | let {content} = OTPReceiver.receive(); 356 | ``` 357 | 358 | This could work equally well compared to `navigator.sms.receive()`, but would pollute the global namespace rather than the `navigator` namespace. 359 | 360 | The other consideration here is whether to enable aborting the request or maybe pass a custom timeout: 361 | 362 | ```javascript 363 | let abort = new AbortController(); 364 | setTimeout(() => { 365 | // abort after two minutes 366 | abort.abort(); 367 | }, 2 * 60 * 1000); 368 | 369 | try { 370 | let {content} = await navigator.sms.receive(abort); 371 | } catch (e) { 372 | // deal with errors 373 | } 374 | ``` 375 | 376 | # Acknowledgements 377 | 378 | This is a write down of a variety of ideas coming from huge amount of people, in no particular order: @sso-google, @ayuishii, @agektmr, @slightlyoff, @reillyeon, @inexorabletash and @rmondello and team. 379 | -------------------------------------------------------------------------------- /index.bs: -------------------------------------------------------------------------------- 1 |
  2 | Title: WebOTP API
  3 | Shortname: webotp
  4 | Level: 1
  5 | Status: CG-DRAFT
  6 | Group: WICG
  7 | ED: http://wicg.github.io/WebOTP
  8 | Repository: WICG/WebOTP
  9 | Editor: Sam Goto, Google Inc. https://google.com, goto@google.com
 10 | Favicon: logo-otp.png
 11 | Markup Shorthands: markdown yes, css no, biblio yes, macros-in-autolinks yes
 12 | Text Macro: FALSE false
 13 | Text Macro: RP Relying Party
 14 | Text Macro: RPS Relying Parties
 15 | Text Macro: TRUE true
 16 | Abstract: A Javascript API to request one time passwords for verifying credentials (e.g. phone numbers, emails).
 17 | Test Suite: https://github.com/web-platform-tests/wpt/tree/master/sms
 18 | 
19 | 20 |
 21 | spec: ecma262; urlPrefix: https://tc39.github.io/ecma262/
 22 |     type: dfn
 23 |         text: time values; url: sec-time-values-and-time-range
 24 |         text: promise; url: sec-promise-objects
 25 | 
 26 | spec: credential-management-1; urlPrefix: https://w3c.github.io/webappsec-credential-management/
 27 |     type: dictionary
 28 |         text: CredentialRequestOptions; url: dictdef-credentialrequestoptions
 29 |     for: Credential
 30 |         type: method
 31 |             text: [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
 32 |             text: [[Create]](origin, options, sameOriginWithAncestors)
 33 |             text: [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
 34 |             text: [[Store]](credential, sameOriginWithAncestors)
 35 |     type: dfn
 36 |         text: signal
 37 |         text: same-origin with its ancestors; url: same-origin-with-its-ancestors
 38 | 
 39 | 
40 | 41 | 46 | 47 | 61 | 62 | logo 64 | 65 | 66 | # Introduction # {#intro} 67 | 68 | 69 | *This section is non-normative.* 70 | 71 | Many web sites need to verify credentials (e.g. phone numbers and 72 | email addresses) as part of their authentication flows. They currently 73 | rely on sending one-time-passwords (OTP) to these communication channels to 74 | be used as proof of ownership. The one-time-password is manually 75 | handed back by the user (typically by copying/pasting) to the web app 76 | which is onerous and erroneous. 77 | 78 | This a proposal for a client side javascript API that enables web 79 | sites to request OTPs and a set of transport-specific conventions (we 80 | start with SMS while leaving the door open to others) that can be used 81 | in coordination with browsers. 82 | 83 | 84 | ## The client side API ## {#intro-client} 85 | 86 | 87 | In this proposal, websites have the ability to call a browser API to 88 | request OTPs coming from specific transports (e.g. via SMS). 89 | 90 | The browser intermediates the receipt of the SMS and the handing off 91 | to the calling website (typically asking for the user's consent), so 92 | the API returns a promise asynchronously. 93 | 94 |
95 | ```js 96 | let {code, type} = await navigator.credentials.get({ 97 | otp: { 98 | transport: ["sms"] 99 | } 100 | }); 101 | ``` 102 |
103 | 104 | 105 | ## The server side API ## {#intro-server} 106 | 107 | 108 | Once the client side API is called, the website's server can send 109 | OTPs to the client via the requested transport mechanisms. For each 110 | of these transport mechanism, a server side convention is set in 111 | place to guarantee that the OTP is delivered safely and 112 | programatically. 113 | 114 | For SMS, for example, servers should send [=origin-bound one-time code messages=] to clients. [[sms-one-time-codes]] 115 | 116 |
117 | 118 | 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"`. 119 | 120 | ``` 121 | "Your authentication code is 123456. 122 | 123 | @example.com #123456" 124 | ``` 125 | 126 |
127 | 128 | 129 | ## Feature Detection ## {#intro-feature-detection} 130 | 131 | 132 | Not all user agents necessarily need to implement the WebOTP API at 133 | the exact same moment in time, so websites need a mechanism to detect 134 | if the API is available. 135 | 136 | Websites can check for the presence of the OTPCredential global 137 | interface: 138 | 139 |
140 | ```js 141 | if (!window.OTPCredential) { 142 | // feature not available 143 | return; 144 | } 145 | ``` 146 |
147 | 148 | 149 | ## Web Components ## {#intro-wc} 150 | 151 | 152 | For the most part, OTP verification largely relies on: 153 | 154 | - input, forms and copy/paste, on the client side and 155 | - third party frameworks to send SMS, on the server side. 156 | 157 | We expect some of these frameworks to develop declarative versions of 158 | this API to facilitate the deployment of their customer's existing 159 | code. 160 | 161 |
162 | Web Component Polyfills 163 | 164 | ```html 165 | 166 | 167 |
168 | 169 | 170 |
171 | ``` 172 |
173 | 174 | And here is an example of how a framework could implement it 175 | using web components: 176 | 177 |
178 | Web Component Polyfills 179 | 180 | ```js 181 | customElements.define("one-time-code", 182 | class extends HTMLInputElement { 183 | connectedCallback() { 184 | this.receive(); 185 | } 186 | async receive() { 187 | let {code, type} = await navigator.credentials.get({ 188 | otp: { 189 | transport: ["sms"] 190 | } 191 | }); 192 | this.value = otp; 193 | this.form.submit(); 194 | } 195 | }, { 196 | extends: "input" 197 | }); 198 | ``` 199 |
200 | 201 | 202 | ## Abort API ## {#intro-abort} 203 | 204 | 205 | Many modern websites handle navigations on the client side. So, if a 206 | user navigates away from an OTP flow to another flow, the request 207 | needs to be cancelled so that the user isn't bothered with a 208 | permission prompt that isn't relevant anymore. 209 | 210 | To facilitate that, an abort controller can be passed to abort the 211 | request: 212 | 213 |
214 | ```js 215 | const abort = new AbortController(); 216 | 217 | setTimeout(() => { 218 | // abort after two minutes 219 | abort.abort(); 220 | }, 2 * 60 * 1000); 221 | 222 | let {code, type} = await navigator.credentials.get({ 223 | signal: abort.signal, 224 | otp: { 225 | transport: ["sms"] 226 | } 227 | }); 228 | ``` 229 |
230 | 231 | 232 | # Client Side API # {#API} 233 | 234 | 235 | Websites call navigator.credentials.get({otp:..., ...}) to retrieve an OTP. 236 | 237 | The algorithm of {{CredentialsContainer/get()|navigator.credentials.get()}} looks through all of the interfaces that inherit from {{Credential}} in the Request a `Credential` abstract operation. 238 | 239 | In that operation, it finds {{OTPCredential}} which inherits from {{Credential}}. It calls 240 | OTPCredential.{{OTPCredential/[[CollectFromCredentialStore]]()}} to collect any [=credentials=] that 241 | should be available without [=user mediation=], and if it does not find 242 | exactly one of those, it then calls OTPCredential.{{OTPCredential/[[DiscoverFromExternalSource]]()}} to have 243 | the user select a credential source and fulfill the request. 244 | 245 | Since this specification requires an [=authorization gesture=] to create OTP [=credentials=], the OTPCredential.{{OTPCredential/[[CollectFromCredentialStore]]()}} [=internal method=] inherits the default behavior of 246 | {{Credential/[[CollectFromCredentialStore]]()|Credential.[[CollectFromCredentialStore]]()}}, of returning an empty set. 247 | 248 | It is then the responsibility of OTPCredential.{{OTPCredential/[[DiscoverFromExternalSource]]()}} to provide an OTP. 249 | 250 | 251 | ## The OTPCredential Interface ## {#OTPCredential} 252 | 253 | 254 | The {{OTPCredential}} interface extends {{Credential}} and contains 255 | the attributes that are returned to the caller when a new one time 256 | password is retrieved. 257 | 258 | {{OTPCredential}}'s [=interface object=] inherits {{Credential}}'s implementation of 259 | {{Credential/[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)}}, and defines its own 260 | implementation of {{OTPCredential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)}}. 261 | 262 | 263 | [Exposed=Window, SecureContext] 264 | interface OTPCredential : Credential { 265 | readonly attribute DOMString code; 266 | }; 267 | 268 | 269 |
270 | : {{Credential/id}} 271 | :: This attribute is inherited from {{Credential}} 272 | 273 | : \[[type]] 274 | :: The {{OTPCredential}} [=interface object=]'s {{Credential/[[type]]}} [=internal slot=]'s value is the string 275 | "`otp`". 276 | 277 | : {{OTPCredential/code}} 278 | :: The retrieved one time password. 279 | 280 |
281 | 282 | 283 | 284 | ### The \[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) Method ### {#sctn-discover-from-external-source} 285 | 286 | 287 | 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|.{{CredentialRequestOptions/otp}} is passed). 288 | 289 | This [=internal method=] accepts three arguments: 290 | 291 |
292 | 293 | : origin 294 | :: This argument is the [=relevant settings object=]'s [=environment settings object/origin=], as determined by the 295 | calling {{CredentialsContainer/get()}} implementation, i.e., {{CredentialsContainer}}'s Request a `Credential` abstract operation. 296 | 297 | : options 298 | :: This argument is a {{CredentialRequestOptions}} object whose 299 | |options|.{{CredentialRequestOptions/otp}} member contains a {{OTPCredentialRequestOptions}} 300 | object specifying the desired attributes of the OTP to retrieve. 301 | 302 | : sameOriginWithAncestors 303 | :: This argument is a Boolean value which is [TRUE] if and only if the caller's [=environment settings object=] is 304 | [=same-origin with its ancestors=]. It is [FALSE] if caller is cross-origin. 305 | 306 | Note: Invocation of this [=internal method=] indicates that it was allowed by 307 | [=permissions policy=], which is evaluated at the [[!CREDENTIAL-MANAGEMENT-1]] level. 308 | See [[#sctn-permissions-policy]]. 309 |
310 | 311 | Note: This algorithm is synchronous: the {{Promise}} resolution/rejection is handled by 312 | {{CredentialsContainer/get()|navigator.credentials.get()}}. 313 | 314 | When this method is invoked, the user agent MUST execute the following algorithm: 315 | 316 | 1. Assert: |options|.{{CredentialRequestOptions/otp}} is [=present=]. 317 | 1. Let |options| be the value of |options|.{{CredentialRequestOptions/otp}}. 318 | 1. Let |callerOrigin| be {{OTPCredential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)/origin}}. 319 | If |callerOrigin| is an [=opaque origin=], throw a {{DOMException}} whose name is "{{NotAllowedError}}", and terminate this algorithm. 320 | 1. Let |effectiveDomain| be the |callerOrigin|'s [=effective domain=]. 321 | If [=effective domain=] is not a [=valid domain=], then throw a 322 | {{DOMException}} whose name is "{{SecurityError}}" and terminate this algorithm. 323 | 324 | Note: An [=effective domain=] may resolve to a [=host=], which can be represented in various manners, 325 | such as [=domain=], [=ipv4 address=], [=ipv6 address=], [=opaque host=], or [=empty host=]. 326 | Only the [=domain=] format of [=host=] is allowed here. This is for simplification and also is 327 | in recognition of various issues with using direct IP address identification in concert with 328 | PKI-based security. 329 | 1. If the |options|.{{CredentialRequestOptions/signal}} is [=present=] and 330 | [=AbortSignal/aborted=], throw the |options|.{{CredentialRequestOptions/signal}}'s 331 | [=AbortSignal/abort reason=] and terminate this algorithm. 332 | 1. TODO(goto): figure out how to connect the dots here with the transport algorithms. 333 | 334 | 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. 335 | 336 | 337 | ## `CredentialRequestOptions` ## {#CredentialRequestOptions} 338 | 339 | 340 | To support obtaining OTPs via {{CredentialsContainer/get()|navigator.credentials.get()}}, 341 | this document extends the {{CredentialRequestOptions}} dictionary as follows: 342 | 343 | 344 | partial dictionary CredentialRequestOptions { 345 | OTPCredentialRequestOptions otp; 346 | }; 347 | 348 | 349 |
350 | : otp 351 | :: This OPTIONAL member is used to make WebOTP requests. 352 |
353 | 354 | 355 | ## `OTPCredentialRequestOptions` ## {#OTPCredentialRequestOptions} 356 | 357 | 358 | The {{OTPCredentialRequestOptions}} dictionary supplies 359 | {{CredentialsContainer/get()|navigator.credentials.get()}} with the data it needs to retrieve an 360 | OTP. 361 | 362 | 363 | dictionary OTPCredentialRequestOptions { 364 | sequence<OTPCredentialTransportType> transport = []; 365 | }; 366 | 367 | 368 |
369 | : transport 370 | :: This OPTIONAL member contains a hint as to how the [=server=] might receive the OTP. 371 | The values SHOULD be members of {{OTPCredentialTransportType}} but [=client platforms=] MUST ignore unknown values. 372 |
373 | 374 | 375 | ## `OTPCredentialTransportType` ## {#enum-transport} 376 | 377 | 378 | 379 | enum OTPCredentialTransportType { 380 | "sms", 381 | }; 382 | 383 | 384 |
385 | User Agents may implement various transport mechanisms to allow 386 | the retrieval of OTPs. This enumeration defines hints as to how 387 | user agents may communicate with the transport mechanisms. 388 | 389 | : sms 390 | :: Indicates that the OTP is expected to arrive via SMS. 391 |
392 | 393 | 394 | ## Permissions Policy integration ## {#sctn-permissions-policy} 395 | 396 | 397 | This specification defines one [=policy-controlled feature=] identified by 398 | the feature-identifier token "otp-credentials". 399 | Its [=default allowlist=] is 'self'. [[!Permissions-Policy]] 400 | 401 | A {{Document}}'s [=Document/permissions policy=] determines whether any content in that document is 402 | [=allowed to use|allowed to successfully invoke=] the [=WebOTP API=], i.e., via 403 | navigator.credentials.get({otp: { transport: ["sms"]}}). 404 | If disabled in any document, no content in the document will be [=allowed to use=] 405 | the foregoing methods: attempting to do so will [return an error](https://www.w3.org/2001/tag/doc/promises-guide#errors). 406 | 407 | 408 | ## Using WebOTP within iframe elements ## {#sctn-iframe-guidance} 409 | 410 | 411 | The [=WebOTP API=] is available in inner frames when the origins match but it's disabled by default in cross-origin <{iframe}>s. 412 | 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 <{iframe/allow}> attribute on the <{iframe}> element and include the [=otp-credentials-feature|otp-credentialst=] feature-identifier token in the <{iframe/allow}> attribute's value. 413 | 414 | [=[RPS]=] utilizing the WebOTP API in an embedded context should review [[#sctn-seccons-visibility]] regarding [=UI redressing=] and its possible mitigations. 415 | 416 | 417 | # Transports # {#transports} 418 | 419 | 420 | We expect a variety of different transport mechanisms to enable OTPs 421 | to be received, most notably via SMS, email and hardware devices. 422 | 423 | Each of these transport mechanisms will need their own conventions on 424 | how to provide OTPs to the browser. 425 | 426 | In this draft, we leave the API surface to be extensible to any number 427 | of transports. 428 | 429 | 430 | ## SMS ## {#transports-sms} 431 | 432 | 433 | One of the most commonly used transport mechanisms for OTP is via 434 | SMS messages, allowing developers to verify phone numbers. They 435 | are typically sent embedded in an SMS message, which gets copied and 436 | pasted by users. 437 | 438 | [[sms-one-time-codes]] defines [=origin-bound one-time code messages=], a format for sending OTPs over SMS and associating them with origins. 439 | 440 | 441 | # Security # {#security} 442 | 443 | 444 | From a security perspective, there are two considerations with this 445 | API: 446 | 447 | * tampering: preventing attacks based on the modification of the message. 448 | 449 | 450 | ## Availability ## {#security-availability} 451 | 452 | 453 | This API is only available on: 454 | 455 | * https (or localhost, for development purposes) 456 | 457 | This API is also only available via https or localhost (for development 458 | purposes). We don't entirely adopt the concept of trustworthy urls 459 | because it covers more schemes (e.g. data://123) than we would like to 460 | (our initial intuition is that (a) https and localhost covers most 461 | cases and (b) it needs to be clear to the user what public facing 462 | entity its sending the SMS). 463 | 464 | 465 | ## Addressing ## {#security-addressing} 466 | 467 | 468 | Each transport mechanism is responsible for guaranteeing that the 469 | browser has enough information to route the OTP appropriately to the 470 | intended origin. 471 | 472 | For example, [=origin-bound one-time code messages=] explicitly 473 | identify the origin on which the OTP can be used. 474 | 475 | The addressing scheme must be enforced by the agent to guarantee that 476 | it gets routed appropriately. 477 | 478 | 479 | ## Tampering ## {#security-tampering} 480 | 481 | 482 | There isn't any built-in cryptographic guarantee that the OTP that is 483 | being handed back by this API hasn't been tampered with. For example, 484 | an attacker could send an [=origin-bound one-time code message=] to the 485 | user's phone with an arbitrary origin which the agent happilly passes 486 | back to the requesting call. 487 | 488 |
489 | ``` 490 | Your verification code is: MUAHAHAHA 491 | 492 | @example.com #MUAHAHAHA 493 | ``` 494 |
495 | 496 | It is the responsibility for the caller to: 497 | 498 | * put in place the checks necessary to verify that the OTP that was received 499 | is a valid one, for example: 500 | * parsing it carefully according to its known formatting expectations 501 | (e.g. only alpha numeric values), 502 | * storing and checking OTPs that were sent on a server side database. 503 | * degrade gracefully when an invalid OTP is received (e.g. re-request one). 504 | 505 | 506 | ## Visibility Considerations for Embedded Usage ## {#sctn-seccons-visibility} 507 | 508 | 509 | Simplistic use of WebOTP in an embedded context, e.g., within <{iframe}>s as described in [[#sctn-iframe-guidance]], may make users vulnerable to UI Redressing attacks, also known as "[Clickjacking](https://en.wikipedia.org/wiki/Clickjacking)". This is where an attacker overlays their own UI on top of a [=[RP]=]'s intended UI and attempts to trick the user into performing unintended actions with the [=[RP]=]. For example, using these techniques, an attacker might be able to trick users into purchasing items, transferring money, etc. 510 | 511 | 512 | # Privacy # {#privacy} 513 | 514 | 515 | From a privacy perspective, the most notable consideration is for a user agent 516 | to enforce the consensual exchange of information between the user and the 517 | website. 518 | 519 | Specifically, this API allows the programatic verification of personally 520 | identifiable attributes of the user, for example email addresses and phone 521 | numbers. 522 | 523 | The attack vector that is most frequently raised is a targeted attack: 524 | websites trying to find a **very** specific user accross all of its user 525 | base. In this attack, if left unattended, a website can use this API to 526 | try to find a **specific** user that owns a **specific** phone number by 527 | sending all / some (depending on the confidence level) of its users an 528 | [=origin-bound one-time code message=] and detecting when one is received. 529 | 530 | Notably, this API doesn't help with the **acquisition** of the personal 531 | information, but rather with its **verification**. That is, this API helps 532 | **verifying** whether the user owns a specific phone number, but doesn't 533 | help acquiring the phone number in the first place (it assumes that the 534 | website already has access to it). 535 | 536 | Nonetheless, the verification of the possession of these attributes is extra 537 | information about the user and should be handled responsibly by a user 538 | agent, typically via **permission prompts** before handing back the OTP 539 | to the website. 540 | 541 | 542 | # Acknowledgements # {#acknowledgements} 543 | 544 | 545 | Many thanks to 546 | Steven Soneff, 547 | Ayu Ishii, 548 | Reilly Grant, 549 | Eiji Kitamura, 550 | Alex Russell, 551 | Owen Campbell-Moore, 552 | Joshua Bell, 553 | Ricky Mondello and 554 | Mike West 555 | for helping craft this proposal. 556 | 557 | Special thanks to Tab Atkins, Jr. for creating and maintaining 558 | [Bikeshed](https://github.com/tabatkins/bikeshed), the specification 559 | authoring tool used to create this document, and for his general 560 | authoring advice. 561 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebOTP API 5 | 6 | 7 | 8 | 9 | 10 | 11 | 25 | 87 | 196 | 225 | 266 | 276 | 328 | 391 | 581 | 582 |
583 |

584 |

WebOTP API

585 |

Draft Community Group Report,

586 |
587 |
588 |
This version: 589 |
http://wicg.github.io/WebOTP 590 |
Test Suite: 591 |
https://github.com/web-platform-tests/wpt/tree/master/sms 592 |
Issue Tracking: 593 |
GitHub 594 |
Editor: 595 |
(Google Inc.) 596 |
597 |
598 |
599 | 601 |
602 |
603 |
604 |

Abstract

605 |

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

606 |
607 |
608 |

Status of this document

609 |
610 |

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

616 |

617 |
618 |
619 | 681 |
682 |

logo

683 |

1. Introduction

684 |

This section is non-normative.

685 |

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

691 |

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

695 |

1.1. The client side API

696 |

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

698 |

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

701 |
702 | 703 |
let {code, type} = await navigator.credentials.get({
 704 |   otp: {
 705 |     transport: ["sms"]
 706 |   }
 707 | });
 708 | 
709 |
710 |

1.2. The server side API

711 |

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

716 |

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

717 |
718 | 719 |

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".

720 |
"Your authentication code is 123456.
 721 | 
 722 | @example.com #123456"
 723 | 
724 |
725 |

1.3. Feature Detection

726 |

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

729 |

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

731 |
732 | 733 |
if (!window.OTPCredential) {
 734 |   // feature not available
 735 |   return;
 736 | }
 737 | 
738 |
739 |

1.4. Web Components

740 |

For the most part, OTP verification largely relies on:

741 | 747 |

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

750 |
751 | Web Component Polyfills 752 |
<script src="sms-sdk.js"></script>
 753 | 
 754 | <form>
 755 |   <input is="one-time-code" required />
 756 |   <input type="submit" />
 757 | </form>
 758 | 
759 |
760 |

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

762 |
763 | Web Component Polyfills 764 |
customElements.define("one-time-code",
 765 |   class extends HTMLInputElement {
 766 |     connectedCallback() {
 767 |       this.receive();
 768 |     }
 769 |     async receive() {
 770 |       let {code, type} = await navigator.credentials.get({
 771 |         otp: {
 772 |          transport: ["sms"]
 773 |         }
 774 |       });
 775 |       this.value = otp;
 776 |       this.form.submit();
 777 |     }
 778 |   }, {
 779 |     extends: "input"
 780 | });
 781 | 
782 |
783 |

1.5. Abort API

784 |

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

788 |

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

790 |
791 | 792 |
const abort = new AbortController();
 793 | 
 794 | setTimeout(() => {
 795 |   // abort after two minutes
 796 |   abort.abort();
 797 | }, 2 * 60 * 1000);
 798 |   
 799 | let {code, type} = await navigator.credentials.get({
 800 |   signal: abort.signal,
 801 |   otp: {
 802 |     transport: ["sms"]
 803 |   }
 804 | });
 805 | 
806 |
807 |

2. Client Side API

808 |

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

809 |

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

810 |

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

814 |

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.

815 |

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

816 |

2.1. The OTPCredential Interface

817 |

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

820 |

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

822 |
[Exposed=Window, SecureContext]
 823 | interface OTPCredential : Credential {
 824 |     readonly attribute DOMString code;
 825 | };
 826 | 
827 |
828 |
id 829 |
830 |

This attribute is inherited from Credential

831 |
[[type]] 832 |
833 |

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

835 |
code, of type DOMString, readonly 836 |
837 |

The retrieved one time password.

838 |
839 |

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

840 |

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

841 |

This internal method accepts three arguments:

842 |
843 |
origin 844 |
845 |

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

847 |
options 848 |
849 |

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

850 |
sameOriginWithAncestors 851 |
852 |

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.

853 |

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

855 |
856 |

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

857 |

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

858 |
    859 |
  1. 860 |

    Assert: options.otp is present.

    861 |
  2. 862 |

    Let options be the value of options.otp.

    863 |
  3. 864 |

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

    866 |
  4. 867 |

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

    869 |

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

    874 |
  5. 875 |

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

    877 |
  6. 878 |

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

    879 |
880 |

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.

881 |

2.2. CredentialRequestOptions

882 |

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

884 |
partial dictionary CredentialRequestOptions {
 885 |     OTPCredentialRequestOptions otp;
 886 | };
 887 | 
888 |
889 |
890 |
otp, of type OTPCredentialRequestOptions 891 |
892 |

This OPTIONAL member is used to make WebOTP requests.

893 |
894 |
895 |

2.3. OTPCredentialRequestOptions

896 |

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

898 |
dictionary OTPCredentialRequestOptions {
 899 |   sequence<OTPCredentialTransportType> transport = [];
 900 | };
 901 | 
902 |
903 |
904 |
transport, of type sequence<OTPCredentialTransportType>, defaulting to [] 905 |
906 |

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

908 |
909 |
910 |

2.4. OTPCredentialTransportType

911 |
enum OTPCredentialTransportType {
 912 |     "sms",
 913 | };
 914 | 
915 |
916 | User Agents may implement various transport mechanisms to allow 917 | the retrieval of OTPs. This enumeration defines hints as to how 918 | user agents may communicate with the transport mechanisms. 919 |
920 |
sms 921 |
922 |

Indicates that the OTP is expected to arrive via SMS.

923 |
924 |
925 |

2.5. Permissions Policy integration

926 |

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

929 |

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"]}}). 930 | 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.

931 |

2.6. Using WebOTP within iframe elements

932 |

The WebOTP API is available in inner frames when the origins match but it’s disabled by default in cross-origin iframes. 933 | 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.

934 |

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.

935 |

3. Transports

936 |

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

938 |

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

940 |

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

942 |

3.1. SMS

943 |

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

947 |

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

948 |

4. Security

949 |

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

951 | 955 |

4.1. Availability

956 |

This API is only available on:

957 | 961 |

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

967 |

4.2. Addressing

968 |

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

971 |

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

973 |

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

975 |

4.3. Tampering

976 |

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

981 |
982 | 983 |
Your verification code is: MUAHAHAHA
 984 | 
 985 | @example.com #MUAHAHAHA
 986 | 
987 |
988 |

It is the responsibility for the caller to:

989 | 1003 |

4.4. Visibility Considerations for Embedded Usage

1004 |

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.

1005 |

5. Privacy

1006 |

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

1009 |

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

1012 |

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

1017 |

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

1021 |

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

1025 |

6. Acknowledgements

1026 |

Many thanks to 1027 | Steven Soneff, 1028 | Ayu Ishii, 1029 | Reilly Grant, 1030 | Eiji Kitamura, 1031 | Alex Russell, 1032 | Owen Campbell-Moore, 1033 | Joshua Bell, 1034 | Ricky Mondello and 1035 | Mike West 1036 | for helping craft this proposal.

1037 |

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

1040 |
1041 |
1042 |

Conformance

1043 |

Document conventions

1044 |

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

1052 |

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

1054 |

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

1058 |
1059 | 1060 |

This is an example of an informative example.

1061 |
1062 |

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

1066 |

Note, this is an informative note.

1067 |

Conformant Algorithms

1068 |

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

1074 |

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

1081 |
1082 | 1083 |

Index

1084 |

Terms defined by this specification

1085 | 1099 | 1106 | 1113 | 1119 | 1126 | 1133 | 1139 | 1149 | 1155 | 1161 | 1167 | 1173 | 1179 | 1185 | 1191 | 1197 | 1203 | 1209 | 1215 | 1221 | 1227 | 1234 | 1240 | 1246 | 1253 | 1259 | 1265 | 1271 | 1281 | 1287 | 1293 | 1299 | 1305 | 1311 | 1317 | 1323 | 1329 | 1335 | 1341 | 1347 | 1353 | 1359 | 1365 |

Terms defined by reference

1366 | 1442 |

References

1443 |

Normative References

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

Informative References

1463 |
1464 |
[URL] 1465 |
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/ 1466 |
1467 |

IDL Index

1468 |
[Exposed=Window, SecureContext]
1469 | interface OTPCredential : Credential {
1470 |     readonly attribute DOMString code;
1471 | };
1472 | 
1473 | partial dictionary CredentialRequestOptions {
1474 |     OTPCredentialRequestOptions otp;
1475 | };
1476 | 
1477 | dictionary OTPCredentialRequestOptions {
1478 |   sequence<OTPCredentialTransportType> transport = [];
1479 | };
1480 | 
1481 | enum OTPCredentialTransportType {
1482 |     "sms",
1483 | };
1484 | 
1485 | 
1486 | 1493 | 1499 | 1507 | 1514 | 1522 | 1528 | 1534 | 1540 | 1546 | 1552 | -------------------------------------------------------------------------------- /logo-otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WICG/web-otp/d9efe8da6035690a513b5293e16669e2f436f675/logo-otp.png -------------------------------------------------------------------------------- /logo-otp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": [80485] 3 | , "contacts": ["cwilso"] 4 | , "repo-type": "cg-report" 5 | } 6 | --------------------------------------------------------------------------------