forked from sso-google/sms-otp-retrieval
-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathindex.bs
560 lines (436 loc) · 23.1 KB
/
index.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
<pre class='metadata'>
Title: WebOTP API
Shortname: webotp
Level: 1
Status: CG-DRAFT
Group: WICG
ED: http://wicg.github.io/WebOTP
Repository: WICG/WebOTP
Editor: Sam Goto, Google Inc. https://google.com, [email protected]
Favicon: logo-otp.png
Markup Shorthands: markdown yes, css no, biblio yes
Text Macro: FALSE <code>false</code>
Text Macro: RP Relying Party
Text Macro: RPS Relying Parties
Text Macro: TRUE <code>true</code>
Abstract: A Javascript API to request one time passwords for verifying credentials (e.g. phone numbers, emails).
Test Suite: https://github.com/web-platform-tests/wpt/tree/master/sms
</pre>
<pre class=anchors>
spec: ecma262; urlPrefix: https://tc39.github.io/ecma262/
type: dfn
text: time values; url: sec-time-values-and-time-range
text: promise; url: sec-promise-objects
spec: credential-management-1; urlPrefix: https://w3c.github.io/webappsec-credential-management/
type: dictionary
text: CredentialRequestOptions; url: dictdef-credentialrequestoptions
for: Credential
type: method
text: [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
text: [[Create]](origin, options, sameOriginWithAncestors)
text: [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
text: [[Store]](credential, sameOriginWithAncestors)
type: dfn
text: signal
text: same-origin with its ancestors; url: same-origin-with-its-ancestors
</pre>
<pre class=link-defaults>
spec:infra; type:dfn; text:list
spec:html; type:dfn; for:environment settings object; text:global object
spec:webidl; type:dfn; text:resolve
</pre>
<style>
dl.domintro dt {
font-family: Menlo, Consolas, "DejaVu Sans Mono", Monaco, monospace;
padding-top: 0.5em;
padding-bottom: 1em;
}
dl.domintro dt a {
color: inherit; border-bottom-style: none;
}
dl.domintro dt code {
font-size: inherit;
}
</style>
<img src="logo-otp.svg" alt="logo"
style="height: 100px; width: 100px; position: absolute; right: 20px; top: 30px;">
<!-- ============================================================ -->
# Introduction # {#intro}
<!-- ============================================================ -->
*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.
<!-- ============================================================ -->
## The client side API ## {#intro-client}
<!-- ============================================================ -->
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.
<div class=example>
```js
let {code, type} = await navigator.credentials.get({
otp: {
transport: ["sms"]
}
});
```
</div>
<!-- ============================================================ -->
## The server side API ## {#intro-server}
<!-- ============================================================ -->
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]]
<div class="example">
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"
```
</div>
<!-- ============================================================ -->
## Feature Detection ## {#intro-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:
<div class=example>
```js
if (!window.OTPCredential) {
// feature not available
return;
}
```
</div>
<!-- ============================================================ -->
## Web Components ## {#intro-wc}
<!-- ============================================================ -->
For the most part, OTP verification largely relies on:
- input, forms and copy/paste, on the client side and
- third party frameworks to send SMS, on the server side.
We expect some of these frameworks to develop declarative versions of
this API to facilitate the deployment of their customer's existing
code.
<div class=example>
Web Component Polyfills
```html
<script src="sms-sdk.js"></script>
<form>
<input is="one-time-code" required />
<input type="submit" />
</form>
```
</div>
And here is an example of how a framework could implement it
using web components:
<div class=example>
Web Component Polyfills
```js
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"
});
```
</div>
<!-- ============================================================ -->
## Abort API ## {#intro-abort}
<!-- ============================================================ -->
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:
<div class=example>
```js
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"]
}
});
```
</div>
<!-- ============================================================ -->
# Client Side API # {#API}
<!-- ============================================================ -->
Websites call <code><a idl for="CredentialsContainer" lt="get()">navigator.credentials.get({otp:..., ...})</a></code> to retrieve an OTP.
The algorithm of {{CredentialsContainer/get()|navigator.credentials.get()}} looks through all of the interfaces that inherit from {{Credential}} in the <a abstract-op>Request a `Credential`</a> abstract operation.
In that operation, it finds {{OTPCredential}} which inherits from {{Credential}}. It calls
<code>OTPCredential.{{OTPCredential/[[CollectFromCredentialStore]]()}}</code> to collect any [=credentials=] that
should be available without [=user mediation=], and if it does not find
exactly one of those, it then calls <code>OTPCredential.{{OTPCredential/[[DiscoverFromExternalSource]]()}}</code> to have
the user select a credential source and fulfill the request.
Since this specification requires an [=authorization gesture=] to create OTP [=credentials=], the <code>OTPCredential.{{OTPCredential/[[CollectFromCredentialStore]]()}}</code> [=internal method=] inherits the default behavior of
{{Credential/[[CollectFromCredentialStore]]()|Credential.[[CollectFromCredentialStore]]()}}, of returning an empty set.
It is then the responsibility of <code>OTPCredential.{{OTPCredential/[[DiscoverFromExternalSource]]()}}</code> to provide an OTP.
<!-- ============================================================ -->
## The OTPCredential Interface ## {#OTPCredential}
<!-- ============================================================ -->
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
{{Credential/[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)}}, and defines its own
implementation of {{OTPCredential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)}}.
<xmp class=idl>
[Exposed=Window, SecureContext]
interface OTPCredential : Credential {
readonly attribute DOMString code;
};
</xmp>
<dl dfn-type="attribute" dfn-for="OTPCredential">
: {{Credential/id}}
:: This attribute is inherited from {{Credential}}
: <dfn>\[[type]]</dfn>
:: The {{OTPCredential}} [=interface object=]'s {{Credential/[[type]]}} [=internal slot=]'s value is the string
"`otp`".
: <dfn>{{OTPCredential/code}}</dfn>
:: The retrieved one time password.
</dl>
<!-- ============================================================ -->
### The <code><dfn for="OTPCredential" method>\[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)</dfn></code> Method ### {#sctn-discover-from-external-source}
<!-- ============================================================ -->
This method is called every time <code><a idl for="CredentialsContainer" lt="get()">navigator.credentials.get({otp:..., ...})</a></code> and is responsible for returning an OTP when one is requested (i.e. when <code>|options|.{{CredentialRequestOptions/otp}}</code> is passed).
This [=internal method=] accepts three arguments:
<dl dfn-type="argument" dfn-for="PublicKeyCredential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)">
: <dfn>origin</dfn>
:: This argument is the [=relevant settings object=]'s [=environment settings object/origin=], as determined by the
calling {{CredentialsContainer/get()}} implementation, i.e., {{CredentialsContainer}}'s <a abstract-op>Request a `Credential`</a> abstract operation.
: <dfn>options</dfn>
:: This argument is a {{CredentialRequestOptions}} object whose
<code>|options|.{{CredentialRequestOptions/otp}}</code> member contains a {{OTPCredentialRequestOptions}}
object specifying the desired attributes of the OTP to retrieve.
: <dfn>sameOriginWithAncestors</dfn>
:: 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 [[#sctn-permissions-policy]].
</dl>
Note: <strong>This algorithm is synchronous:</strong> the {{Promise}} resolution/rejection is handled by
{{CredentialsContainer/get()|navigator.credentials.get()}}.
When this method is invoked, the user agent MUST execute the following algorithm:
1. Assert: <code>|options|.{{CredentialRequestOptions/otp}}</code> is [=present=].
1. Let |options| be the value of <code>|options|.{{CredentialRequestOptions/otp}}</code>.
1. Let |callerOrigin| be {{OTPCredential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)/origin}}.
If |callerOrigin| is an [=opaque origin=], throw a {{DOMException}} whose name is "{{NotAllowedError}}", and terminate this algorithm.
1. Let |effectiveDomain| be the |callerOrigin|'s [=effective domain=].
If [=effective domain=] is not a [=valid domain=], then throw 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.
1. If the <code>|options|.{{CredentialRequestOptions/signal}}</code> is [=present=] and
[=AbortSignal/aborted=], throw the <code>|options|.{{CredentialRequestOptions/signal}}</code>'s
[=AbortSignal/abort reason=] and terminate this algorithm.
1. 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.
<!-- ============================================================ -->
## `CredentialRequestOptions` ## {#CredentialRequestOptions}
<!-- ============================================================ -->
To support obtaining OTPs via {{CredentialsContainer/get()|navigator.credentials.get()}},
this document extends the {{CredentialRequestOptions}} dictionary as follows:
<xmp class="idl">
partial dictionary CredentialRequestOptions {
OTPCredentialRequestOptions otp;
};
</xmp>
<div dfn-type="dict-member" dfn-for="CredentialRequestOptions">
: <dfn>otp</dfn>
:: This OPTIONAL member is used to make WebOTP requests.
</div>
<!-- ============================================================ -->
## `OTPCredentialRequestOptions` ## {#OTPCredentialRequestOptions}
<!-- ============================================================ -->
The {{OTPCredentialRequestOptions}} dictionary supplies
{{CredentialsContainer/get()|navigator.credentials.get()}} with the data it needs to retrieve an
OTP.
<xmp class="idl">
dictionary OTPCredentialRequestOptions {
sequence<OTPCredentialTransportType> transport = [];
};
</xmp>
<div dfn-type="dict-member" dfn-for="OTPCredentialRequestOptions">
: <dfn>transport</dfn>
:: 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.
</div>
<!-- ============================================================ -->
## `OTPCredentialTransportType` ## {#enum-transport}
<!-- ============================================================ -->
<xmp class="idl">
enum OTPCredentialTransportType {
"sms",
};
</xmp>
<div dfn-type="enum-value" dfn-for="OTPCredentialTransportType">
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.
: <dfn>sms</dfn>
:: Indicates that the OTP is expected to arrive via SMS.
</div>
<!-- ============================================================ -->
## Permissions Policy integration ## {#sctn-permissions-policy}
<!-- ============================================================ -->
This specification defines one [=policy-controlled feature=] identified by
the feature-identifier token "<code><dfn data-lt="otp-credentials-feature" export>otp-credentials</dfn></code>".
Its [=default allowlist=] is '<code>self</code>'. [[!Permissions-Policy]]
A {{Document}}'s [=Document/permissions policy=] determines whether any content in that <a href="https://html.spec.whatwg.org/multipage/dom.html#documents">document</a> is
[=allowed to use|allowed to successfully invoke=] the [=WebOTP API=], i.e., via
<code><a idl for="CredentialsContainer" lt="get()">navigator.credentials.get({otp: { transport: ["sms"]}})</a></code>.
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](https://www.w3.org/2001/tag/doc/promises-guide#errors).
<!-- ============================================================ -->
## Using WebOTP within <code>iframe</code> elements ## {#sctn-iframe-guidance}
<!-- ============================================================ -->
The [=WebOTP API=] is available in inner frames when the origins match but it's disabled by default in cross-origin <{iframe}>s.
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 <code>[=otp-credentials-feature|otp-credentialst=]</code> feature-identifier token in the <{iframe/allow}> attribute's value.
[=[RPS]=] utilizing the WebOTP API in an embedded context should review [[#sctn-seccons-visibility]] regarding [=UI redressing=] and its possible mitigations.
<!-- ============================================================ -->
# Transports # {#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.
<!-- ============================================================ -->
## SMS ## {#transports-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.
<!-- ============================================================ -->
# Security # {#security}
<!-- ============================================================ -->
From a security perspective, there are two considerations with this
API:
* tampering: preventing attacks based on the modification of the message.
<!-- ============================================================ -->
## Availability ## {#security-availability}
<!-- ============================================================ -->
This API is only available on:
* https (or localhost, for development purposes)
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).
<!-- ============================================================ -->
## Addressing ## {#security-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.
<!-- ============================================================ -->
## Tampering ## {#security-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.
<div class=example>
```
Your verification code is: MUAHAHAHA
@example.com #MUAHAHAHA
```
</div>
It is the responsibility for the caller to:
* put in place the checks necessary to verify that the OTP that was received
is a valid one, for example:
* parsing it carefully according to its known formatting expectations
(e.g. only alpha numeric values),
* storing and checking OTPs that were sent on a server side database.
* degrade gracefully when an invalid OTP is received (e.g. re-request one).
<!-- ============================================================ -->
## Visibility Considerations for Embedded Usage ## {#sctn-seccons-visibility}
<!-- ============================================================ -->
Simplistic use of WebOTP in an embedded context, e.g., within <{iframe}>s as described in [[#sctn-iframe-guidance]], may make users vulnerable to <dfn>UI Redressing</dfn> 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.
<!-- ============================================================ -->
# Privacy # {#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.
<!-- ====================================================================== -->
# Acknowledgements # {#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](https://github.com/tabatkins/bikeshed), the specification
authoring tool used to create this document, and for his general
authoring advice.