WebOTP API

커뮤니티 그룹 초안 보고서,

현재 버전:
http://wicg.github.io/WebOTP
테스트 스위트:
https://github.com/web-platform-tests/wpt/tree/master/sms
이슈 트래킹:
GitHub
편집자:
(Google Inc.)

요약

자격 증명(예: 휴대폰 번호, 이메일)을 확인하기 위한 일회용 비밀번호를 요청하는 Javascript API입니다.

이 문서의 상태

이 명세는 Web Platform Incubator Community Group에서 발행되었습니다. 이는 W3C 표준이 아니며 W3C 표준 트랙에 포함되어 있지 않습니다. W3C 커뮤니티 기여자 라이선스 계약(CLA)에서는 제한된 옵트아웃 및 기타 조건이 적용됩니다. W3C 커뮤니티 및 비즈니스 그룹에 대해 자세히 알아보세요.

logo

1. 소개

이 섹션은 규범적인 내용이 아닙니다.

많은 웹사이트는 인증 흐름의 일부로 자격 증명(예: 휴대폰 번호와 이메일 주소)을 검증해야 합니다. 현재는 이러한 통신 채널로 일회용 비밀번호(OTP)를 전송하여 소유권 증명에 사용하고 있습니다. 일회용 비밀번호는 사용자가 웹앱에 직접 입력(일반적으로 복사/붙여넣기)해야 하므로 번거롭고 실수하기 쉽습니다.

이 문서는 웹사이트가 OTP를 요청할 수 있게 하는 클라이언트측 자바스크립트 API와, 브라우저와 협력하여 사용할 수 있는 전송 방식별 규약 세트를 제안합니다(우선 SMS로 시작하지만, 다른 방식도 열려 있습니다).

1.1. 클라이언트 측 API

이 제안에서 웹사이트는 특정 전송 방식(SMS 등)을 통해 오는 OTP를 요청하기 위해 브라우저 API를 호출할 수 있습니다.

브라우저는 SMS 수신과 웹사이트로의 전달을 중개하며(일반적으로 사용자 동의를 요청함), API는 비동기적으로 promise를 반환합니다.

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

1.2. 서버 측 API

클라이언트 측 API가 호출되면, 웹사이트의 서버는 요청된 전송 방식으로 클라이언트에 OTP를 전송할 수 있습니다. 각 전송 방식마다 서버 측 규약을 마련하여 OTP가 안전하고 프로그래밍적으로 전달되도록 보장합니다.

예를 들어, SMS의 경우 서버는 origin-bound one-time code 메시지를 클라이언트에 전송해야 합니다. [sms-one-time-codes]

다음 origin-bound one-time code 메시지에서, 호스트는 "example.com", 코드 값은 "123456", 안내문은 "Your authentication code is 123456.\n"입니다.

"Your authentication code is 123456.

@example.com #123456"

1.3. 기능 감지

모든 사용자 에이전트가 WebOTP API를 동일한 시점에 반드시 구현할 필요는 없으므로, 웹사이트는 API 사용 가능 여부를 감지하는 메커니즘이 필요합니다.

웹사이트는 OTPCredential 글로벌 인터페이스 존재 여부를 확인할 수 있습니다:

if (!window.OTPCredential) {
  // 기능 사용 불가
  return;
}

1.4. 웹 컴포넌트

대부분의 경우, OTP 검증은 다음에 크게 의존합니다:

이러한 프레임워크 중 일부는 고객의 기존 코드를 쉽게 배포할 수 있도록 이 API의 선언적 버전을 개발할 것으로 예상됩니다.

웹 컴포넌트 폴리필 예시
<script src="sms-sdk.js"></script>

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

그리고 아래는 웹 컴포넌트를 활용해 프레임워크가 이를 구현하는 예시입니다:

웹 컴포넌트 폴리필 예시
customElements.define("one-time-code",
  class extends HTMLInputElement {
    connectedCallback() {
      this.receive();
    }
    async receive() {
      let {code, type} = await navigator.credentials.get({
        otp: {
         transport: ["sms"]
        }
      });
      this.value = otp;
      this.form.submit();
    }
  }, {
    extends: "input"
});

1.5. Abort API

현대 웹사이트는 클라이언트 측에서 네비게이션을 처리하는 경우가 많습니다. 따라서 사용자가 OTP 흐름에서 다른 흐름으로 이동할 때, 더 이상 관련 없는 권한 요청으로 인해 사용자가 방해받지 않도록 요청을 취소할 필요가 있습니다.

이를 위해 abort controller를 전달하여 요청을 중단할 수 있습니다:

const abort = new AbortController();

setTimeout(() => {
  // 2분 후에 중단
  abort.abort();
}, 2 * 60 * 1000);
  
let {code, type} = await navigator.credentials.get({
  signal: abort.signal,
  otp: {
    transport: ["sms"]
  }
});

2. 클라이언트 측 API

웹사이트는 navigator.credentials.get({otp:..., ...}) 을 호출하여 OTP를 가져옵니다.

navigator.credentials.get() 알고리즘은 Request a Credential 추상 연산에서 Credential을 상속하는 모든 인터페이스를 탐색합니다.

그 과정에서 OTPCredential 을 찾고, 이 인터페이스는 Credential을 상속합니다. OTPCredential.[[CollectFromCredentialStore]]() 을 호출하여 credentials 중 사용자 중재 없이 이용 가능한 것을 수집하며, 만약 정확히 하나를 찾지 못하면 OTPCredential.[[DiscoverFromExternalSource]]() 을 호출하여 사용자가 credential source를 선택하고 요청을 해결할 수 있도록 합니다.

이 명세는 OTP authorization gesture가 필요하므로, OTPCredential.[[CollectFromCredentialStore]]() 내부 메서드Credential.[[CollectFromCredentialStore]]() 의 기본 동작(빈 집합 반환)을 상속합니다.

따라서 OTPCredential.[[DiscoverFromExternalSource]]() 가 OTP를 제공하는 역할을 담당합니다.

2.1. OTPCredential 인터페이스

OTPCredential 인터페이스는 Credential 을 확장하며, 새로운 일회용 비밀번호가 조회될 때 호출자에게 반환되는 속성을 포함합니다.

OTPCredential인터페이스 객체Credential[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors) 구현을 상속하며, [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) 자체 구현을 정의합니다.

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

이 속성은 Credential 에서 상속받은 것입니다.

[[type]]

OTPCredential 인터페이스 객체[[type]] 내부 슬롯 값은 문자열 "otp"입니다.

code, 타입은 DOMString, 읽기 전용

조회된 일회용 비밀번호입니다.

2.1.1. [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) 메서드

이 메서드는 navigator.credentials.get({otp:..., ...}) 이 호출될 때마다 실행되며, OTP가 요청될 경우(options.otp 가 전달될 때) OTP를 반환하는 역할을 합니다.

내부 메서드는 세 개의 인자를 받습니다:

origin

이 인자는 호출한 get() 구현에서 결정된 관련 설정 객체origin입니다.

options

이 인자는 options.otp 멤버가 OTPCredentialRequestOptions 객체를 포함하는 CredentialRequestOptions 객체입니다.

sameOriginWithAncestors

이 인자는 호출자의 environment settings object조상과 동일 출처일 경우에만 true가 되고, 교차 출처일 경우 false입니다.

참고:내부 메서드의 호출은 permissions policy에 의해 허용되었음을 나타내며, 이는 [CREDENTIAL-MANAGEMENT-1] 수준에서 평가됩니다. § 2.5 Permissions Policy integration을 참고하세요.

참고: 이 알고리즘은 동기적입니다: Promise 의 resolve/reject는 navigator.credentials.get()에서 처리됩니다.

이 메서드가 호출되면, 사용자 에이전트는 다음 알고리즘을 반드시 실행해야 합니다:

  1. 단언: options.otp존재한다.

  2. optionsoptions.otp 값으로 설정한다.

  3. callerOriginorigin으로 설정한다. 만약 callerOrigin불투명 출처라면, 이름이 "NotAllowedError"인 DOMException 을 반환하고 알고리즘을 종료한다.

  4. effectiveDomaincallerOrigin유효 도메인으로 설정한다. 유효 도메인유효한 도메인이 아니면, 이름이 "SecurityError"인 DOMException 을 반환하고 알고리즘을 종료한다.

    참고: 유효 도메인호스트로 해석될 수 있으며, 도메인, ipv4 주소, ipv6 주소, 불투명 호스트, 빈 호스트 등으로 표현될 수 있습니다. 여기서는 도메인 형식만 허용됩니다. 이는 단순화를 위한 것이며, PKI 기반 보안과 직접 IP 주소 식별을 함께 사용할 때 발생하는 여러 이슈를 고려한 것입니다.

  5. options.signal존재하며, 그 aborted flagtrue라면, 이름이 "AbortError"인 DOMException 을 반환하고 알고리즘을 종료한다.

  6. TODO(goto): transport 알고리즘과의 연결 방법을 정리해야 함.

위 과정을 진행하는 동안, 사용자 에이전트는 사용자가 OTP를 출처와 공유하는 과정을 안내하는 UI를 보여야 합니다.

2.2. CredentialRequestOptions

navigator.credentials.get() 을 통해 OTP를 획득할 수 있도록, 이 문서는 CredentialRequestOptions 딕셔너리를 아래와 같이 확장합니다:

partial dictionary CredentialRequestOptions {
    OTPCredentialRequestOptions otp;
};
otp, 타입은 OTPCredentialRequestOptions

이 OPTIONAL 멤버는 WebOTP 요청에 사용됩니다.

2.3. OTPCredentialRequestOptions

OTPCredentialRequestOptions 딕셔너리는 navigator.credentials.get() 이 OTP를 획득하는데 필요한 데이터를 제공합니다.

dictionary OTPCredentialRequestOptions {
  sequence<OTPCredentialTransportType> transport = [];
};
transport, 타입은 sequence<OTPCredentialTransportType>, 기본값 []

이 OPTIONAL 멤버는 서버가 OTP를 수신할 수 있는 방법에 대한 힌트를 담고 있습니다. 값은 OTPCredentialTransportType의 멤버여야 하며, 클라이언트 플랫폼은 알 수 없는 값은 무시해야 합니다.

2.4. OTPCredentialTransportType

enum OTPCredentialTransportType {
    "sms",
};
User Agent는 다양한 전송 방식으로 OTP를 가져올 수 있도록 구현할 수 있습니다. 이 열거형은 User Agent가 전송 방식과 소통하는 방법에 대한 힌트를 정의합니다.
sms

OTP가 SMS로 도착할 것으로 예상됨을 나타냅니다.

2.5. Permissions Policy 연동

이 명세는 feature-identifier 토큰 "otp-credentials" 로 식별되는 하나의 정책 제어 기능을 정의합니다. 기본 허용 목록은 'self'입니다. [Permissions-Policy]

Documentpermissions policy는 해당 문서의 모든 콘텐츠가 WebOTP API를 성공적으로 호출할 수 있는지를 결정합니다. 즉, navigator.credentials.get({otp: { transport: ["sms"]}}) 와 같이 호출합니다. 문서에서 비활성화된 경우, 해당 문서의 모든 콘텐츠는 이러한 메서드를 사용할 수 없습니다. 시도할 경우 오류를 반환합니다.

2.6. iframe 요소 내에서 WebOTP 사용하기

WebOTP API는 출처가 일치하는 내프레임에서 사용 가능하지만, 교차 출처 iframe에서는 기본적으로 비활성화되어 있습니다. 이 기본 정책을 재정의하고 교차 출처 iframe 에서 WebOTP API의 [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) 메서드를 호출할 수 있도록 하려면, allow 속성에 otp-credentials feature-identifier 토큰을 포함해 지정해야 합니다.

WebOTP API를 임베디드 환경에서 사용하는 Relying Parties§ 4.4 임베디드 사용에 대한 가시성 고려사항에서 UI 변조 및 그 대응 방안을 반드시 검토해야 합니다.

3. 전송 방식

다양한 전송 방식(SMS, 이메일, 하드웨어 기기 등)을 통해 OTP를 받을 수 있도록 설계되었습니다.

각 전송 방식마다 브라우저에 OTP를 제공하는 방법에 대한 고유한 규약이 필요합니다.

이 초안에서는 API가 여러 전송 방식으로 확장 가능하도록 설계되어 있습니다.

3.1. SMS

OTP를 가장 많이 사용하는 전송 방식 중 하나는 SMS 메시지입니다. 이를 통해 개발자는 휴대폰 번호를 검증할 수 있습니다. 일반적으로 사용자가 SMS로 받은 코드를 복사하여 붙여넣는 방식입니다.

[sms-one-time-codes]에서는 origin-bound one-time code 메시지 형식을 정의하여, OTP를 SMS로 전송하고 특정 출처와 연결할 수 있도록 합니다.

4. 보안

보안 관점에서 이 API에는 두 가지 고려사항이 있습니다:

4.1. 가용성

이 API는 다음 환경에서만 사용할 수 있습니다:

이 API는 https 또는 localhost(개발 목적으로)에서만 사용 가능합니다. 신뢰할 수 있는 URL 개념을 완전히 채택하지 않는 이유는, 신뢰할 수 있는 URL이 우리가 원하는 것보다 더 많은 스킴(예: data://123)을 포함하기 때문입니다. (초기 직관은 (a) https와 localhost가 대부분을 커버하고, (b) SMS를 보내는 공개 엔티티가 사용자에게 명확해야 한다는 점입니다.)

4.2. 주소 지정

각 전송 방식은 브라우저가 OTP를 올바른 출처로 라우팅할 수 있도록 충분한 정보를 제공해야 합니다.

예를 들어, origin-bound one-time code 메시지는 OTP를 사용할 수 있는 출처를 명확히 식별합니다.

주소 지정 방식은 에이전트가 반드시 강제하여 올바르게 라우팅될 수 있도록 해야 합니다.

4.3. 위·변조

이 API가 반환하는 OTP가 위변조되지 않았다는 암호학적 보장이 내장되어 있지 않습니다. 예를 들어, 공격자가 임의의 출처를 가진 origin-bound one-time code 메시지를 사용자의 휴대폰에 보내면, 에이전트는 이를 요청한 호출에 그대로 전달할 수 있습니다.

귀하의 인증 코드는: MUAHAHAHA

@example.com #MUAHAHAHA

호출자는 다음을 책임져야 합니다:

4.4. 임베디드 사용에 대한 가시성 고려사항

WebOTP를 임베디드 환경(예: iframe 내)에서 단순하게 사용할 경우, § 2.6 iframe 요소 내에서 WebOTP 사용하기에서 설명한 대로 UI 변조 공격(일명 "클릭재킹")에 사용자가 취약해질 수 있습니다. 이는 공격자가 의도한 UI 위에 자신의 UI를 겹쳐 사용자로 하여금 Relying Party의 의도와 다른 행동(예: 구매, 송금 등)을 하도록 속이는 공격입니다.

5. 개인정보 보호

개인정보 보호 관점에서 가장 중요한 고려사항은 사용자와 웹사이트 간 정보 교환이 반드시 동의하에 이뤄지도록 사용자 에이전트가 강제해야 한다는 점입니다.

구체적으로 이 API는 사용자의 이메일 주소, 휴대폰 번호 등 개인 식별 속성을 프로그래밍적으로 검증할 수 있게 해줍니다.

가장 흔히 제기되는 공격 벡터는 타겟형 공격입니다: 웹사이트가 전체 사용자 중 특정 사용자를 찾으려 하는 경우입니다. 만약 관리가 제대로 되지 않으면, 웹사이트가 이 API를 이용해 전체/일부 사용자에게 origin-bound one-time code 메시지를 보내고, 언제 수신되는지 감지하여 특정 휴대폰 번호를 가진 사용자를 찾을 수 있습니다.

특히 이 API는 개인정보를 획득하는 데는 도움이 되지 않고, 검증에만 도움을 줍니다. 즉, 사용자가 특정 휴대폰 번호를 소유하고 있는지 검증할 수 있지만, 애초에 번호를 획득하는 데는 사용되지 않습니다(웹사이트가 이미 해당 정보를 갖고 있다고 가정함).

그럼에도, 이러한 속성 소유 검증 자체가 사용자에 대한 추가 정보이므로, 사용자 에이전트는 이를 책임감 있게 취급해야 하며, 일반적으로 OTP를 웹사이트에 반환하기 전에 권한 요청을 통해 처리해야 합니다.

6. 감사의 글

Steven Soneff, Ayu Ishii, Reilly Grant, Eiji Kitamura, Alex Russell, Owen Campbell-Moore, Joshua Bell, Ricky Mondello, Mike West 에게 이 제안서 작성에 도움을 준 것에 깊이 감사드립니다.

특별히 Tab Atkins, Jr.에게 Bikeshed를 만들고 유지관리해 주신 점, 그리고 문서 작성에 대한 조언에 감사드립니다. Bikeshed는 이 문서 작성에 사용된 명세 작성 도구입니다.

준수성

문서 규약

준수 요구사항은 설명적 단언과 RFC 2119 용어의 조합으로 표현됩니다. 이 문서의 규범적 부분에서 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", "OPTIONAL"이라는 핵심 단어들은 RFC 2119에서 설명된 대로 해석해야 합니다. 하지만 가독성을 위해 이 명세에서는 이러한 단어들이 모두 대문자로 나타나지 않습니다.

이 명세의 모든 텍스트는 규범적이며, 명시적으로 비규범적임을 표시한 섹션, 예시, 참고사항만 예외입니다. [RFC2119]

이 명세의 예시는 "예를 들어"라는 문구로 시작하거나, class="example"로 규범적 텍스트와 구분되어 나타납니다. 예시:

이것은 참고용 예시입니다.

참고사항은 "참고"라는 단어로 시작하며, class="note"로 규범적 텍스트와 구분됩니다. 아래와 같이 표시됩니다:

참고: 이것은 참고용 주석입니다.

준수 알고리즘

알고리즘의 일부로 명령형으로 표현된 요구사항 (예: "선행 공백 문자를 모두 제거한다" 또는 "false를 반환하고 이 단계를 중단한다") 는 알고리즘을 도입하는 데 사용된 핵심 단어("must", "should", "may" 등)의 의미로 해석되어야 합니다.

알고리즘이나 특정 단계로 표현된 준수 요구사항은 최종 결과가 동일하기만 하면 어떤 방식으로든 구현할 수 있습니다. 특히, 이 명세에서 정의된 알고리즘은 이해하기 쉽도록 설계된 것이며, 성능을 고려한 것이 아닙니다. 구현자는 최적화를 권장합니다.

색인

이 명세에서 정의된 용어

참고에서 정의된 용어

참고 문헌

규범적 참고 문헌

[CREDENTIAL-MANAGEMENT-1]
Mike West. Credential Management Level 1. 2019년 1월 17일. WD. URL: https://www.w3.org/TR/credential-management-1/
[DOM]
Anne van Kesteren. DOM 표준. 현행 표준. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch 표준. 현행 표준. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML 표준. 현행 표준. URL: https://html.spec.whatwg.org/multipage/
[Permissions-Policy]
Ian Clelland. Permissions Policy. 2020년 7월 16일. WD. URL: https://www.w3.org/TR/permissions-policy-1/
[RFC2119]
S. Bradner. RFC에서 요구 사항 수준을 나타내는 핵심 단어. 1997년 3월. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SMS-ONE-TIME-CODES]
SMS로 전달되는 출처 바인딩 일회용 코드. cg-draft. URL: https://wicg.github.io/sms-one-time-codes/
[WebIDL]
Boris Zbarsky. Web IDL. 2016년 12월 15일. ED. URL: https://heycam.github.io/webidl/

참고용 참고 문헌

[URL]
Anne van Kesteren. URL 표준. 현행 표준. URL: https://url.spec.whatwg.org/

IDL 색인

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

partial dictionary CredentialRequestOptions {
    OTPCredentialRequestOptions otp;
};

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

enum OTPCredentialTransportType {
    "sms",
};