적용관련 가이드

Passkey Client Web API

개요

웹 브라우저 환경에서 Passkey Service를 이용하기 위한 Web API 의 적용방법에 대해서 설명한다.

지원범위

Passkey API(WebAuthentication)를 지원하는 모든 운영환경과 브라우저

사용법

스크립트 로딩

아래와 같이 wizvera-passkeys.js를 <script>태그를 사용하여 로딩한다.

<html> <head> <script src="https://svc.passkeys.kr/client/js/wizvera-passkeys.js"></script> </head> <body> ... </body> </html>

옵션 설정 및 초기화

스크립트 로딩이 완료되면 WizveraPasskeys class 를 사용하여 client API 인스턴스를 생성한다. 옵션은 인스턴스 생성시 반드시 설정되어야 하며, 한번 설정되에 생성된 인스턴스의 옵션은 변경할 수 없다. 옵션을 변경하려면 변경된 옵션으로 인스턴스를 새로 생성하여야 한다.

var options = { passkeyServiceRoot: 'https://svc.passkeys.kr/webauthn/api/v1', clientId: 'service1', origin: 'your-service-domain.com', ... }; // 옵션은 반드시 설정되어야 한다. var passkeysClient = new WizveraPasskeys(options);

브라우저 지원여부 검사

WizveraPasskeys 클래스의 static 함수인 IsSupportPasskey()를 사용하여 현재 브라우저의 지원여부를 검사할 수 있다.

try { // 지원하지 않는 브라우저면 NotSupportError예외가 발생한다. WizveraPasskeys.IsSupportPasskey(); // 지원하는 경우 초기화 수행 ... } catch(error) { if(error.name === 'NotSupportError') { alert('Passkey를 지원하지 않는 브라우저 입니다.'); return; } }

초기화 옵션

설정가능한 모든 옵션

{ passkeyServiceRoot: '/', // (m)<string>: passkey api service root: https://passkeys.kr/webauthn/api/v1 clientId: '', // (m)<string>: registered service id origin: '', // (m)<string>: client origin email: '', // (o)<string>: user email phoneNumber: '', // (o)<string>: user phoneNumber userHandle: '', // (o)<string>: userHandle qrToken: '', // (o)<string>: qrToken qrTokenCheckInterval: 3000, // (o)<number>: qrToken check interval milliseconds useExternalQRCodeLibrary: false, // (o)<boolean>: use external qrcode library //qrTokenClassifier: function() {}, // (o)<function>:qr token classifier qrCodeOptions: { // (o)<object>: qrcode options <https://github.com/soldair/node-qrcode> //version //errorCorrectionLevel: 'M', //maskPattern //margin: 4, //scale: 4, //small: false, width: 300, color: { //dark: #000000ff, //light: #ffffffff } }, // qrTokenProcessURL: '', // (o)<string>:QRToken 처리할 페이지 URL // qrCodeHandler: function() {}, // (o)<function>:QRCode generator handler };
// true를 리턴하면 QR코드를 사용, false를 리턴하면 장치에 내장된 Passkey Authenticator를 사용 options.qrTokenClassifier = function() { // userAgent에 Windows가 포함되면 QR코드 사용 if(navigator.userAgent.indexOf('Windows') > 0) return true; else return false; }
// QR코드가 생성되면 호출되는 함수, useExternalQRCodeLibrary가 true면 qrCodeCanvas는 undefined options.qrCodeHandler = function(qrCodeData, qrCodeCanvas) { console.log(qrCodeData); var qrCodeContainer = document.getElementById('qrcode'); qrCodeContainer.innerHTML = ''; var text = document.createElement('h5'); text.innerText = qrCodeData; qrCodeContainer.appendChild(text); // useExternalQRCodeLibrary true면 qrCodeCanvas는 undefined가 리턴됨 if(options.useExternalQRCodeLibrary !== true) { qrCodeContainer.appendChild(qrCodeCanvas); } }

사용자 등록 및 검증

Passkey를 서버에 등록 및 사용하려면 먼저 Passkey 서버에 사용자등록을 하여야 한다. Passkey 서버에 사용자를 등록하려면 Email로 발송된 일회용 인증코드를 입력 받는 절차를 사용하여 이메일의 실체여부를 검증 받아야 한다. 이메일 검증 절차는 checkUser(), verifyAuthCode()를 통하여 처리한다.

사용자 등록 확인 및 인증코드 발송

사용자 등록 확인 및 인증코드 발송은 checkUser() 함수를 사용한다.

var options = { email: userEmail }; passkeysClient.checkUser(options);
function checkEmail() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); // 입력된 email의 서버 등록여부 확인 // 등록된 사용자가 아니면 자동으로 인증코드가 발송되며 ServiceError예외(에러코드 1005)가 발생한다. passkeysClient.checkUser({ email: email }).then(function(res) { // 등록된 email이면 res는 항상 true를 리턴함 writeLog('등록된 사용자입니다.'); }).catch(function(error) { if(error.name === 'ServiceError' && error.code === 1005) { // 인증코드가 발송됨. 인증코드 입력후 검증 } }); }

인증코드 확인

인증코드 확인은 verifyAuthCode() 함수를 사용한다.

var options = { authCode: '112233' }; passkeysClient.verifyAuthCode(options);
function checkCode() { if(!passkeysClient) { alert('initialize first'); return; } var authCode = document.getElementById('authCode').value.trim(); passkeysClient.verifyAuthCode({ authCode: authCode }).then(function(res) { // 인증코드가 확인되면 res는 항상 true를 리턴함. // 인증코드 확인. 사용자 email이 Passkey 서버에 등록됨 }).catch(function(error) { errorHandler(error); }); }

사용자 이메일 재확인

사용자 재인증 절차 등 필요에 의해서 등록된 사용자의 이메일을 재검증할 필요가 있을때 requestAuthCode()를 사용한다.

var options = { email: userEmail }; passkeysClient.requestAuthCode(options);
function requestCode() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); passkeysClient.requestAuthCode({ email: email }).then(function(res) { // email로 인증코드가 전송되었음 }).catch(function(error) { errorHandler(error); }); }

새 Passkey 생성 및 등록

Passkey를 통하여 서버에 로그인 및 인증을 하려면 먼저 Passkey를 서버에 등록하여야 한다. 새로운 Passkey를 생성 및 서버에 등록은 register() 함수를 통하여 진행한다.

var registerOptions = { email: userEmail, phoneNumber: userPhoneNumber }; passkeysClient.register(registerOptions);
function register() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); var phoneNumber = document.getElementById('phoneNumber').value.trim(); passkeysClient.register({ email: email, phoneNumber: phoneNumber }).then(function(result) { // Passkey 서버에 등록 완료 // result.callbackURL 호출하여 서비스 서버의 사용자DB 인증정보와 동기화 필요 }).catch(function(error) { errorHandler(error); }); }

Passkey 인증 및 검증

서버에 로그인 및 인증을 하려면 등록된 Passkey를 사용하여 서명값을 생성하고 그것을 서버로부터 검증 받아야 한다. Passkey 서명 생성 및 검증은 assertion() 함수를 사용한다.

var assertionOptions = { userHandle: userHandle // 2차인증이 필요할 때 }; passkeysClient.assertion(assertionOptions);
function assertion() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); var phoneNumber = document.getElementById('phoneNumber').value.trim(); passkeysClient.assertion({ email: email, phoneNumber: phoneNumber }).then(function(result) { // Passkey 서버로부터 서명검증 완료 // result.callbackURL 호출하여 서비스 서버에 사용자 로그인 처리 할 수 있음 }).catch(function(error) { errorHandler(error); }); }

callbackURL을 통한 등록/인증 처리

Passkey 등록이나 인증절차는 Passkey 서버에서 관리되고 실제 서비스 서버 및 서비스 서버의 사용자 인증 DB와는 무관하다. 그러므로 Passkey 서버를 통하여 등록 및 인증이 완료되면 그 처리 결과를 서비스 서버로 전달하여 실제 서비스 서버의 사용자 정보와 동기화 절차가 필요하다.
Passkey 서비스 신청시 callbackURL을 등록해놓으면 그 callbackURL을 통하여 Passkey 처리 결과를 조회할 수 있는 파라미터가 전달된다.
register()나 assertion() 함수 호출이 성공하면 결과 object에 callbackURL이 리턴되는데 그 URL을 호출하여 서비스 서버에서 Passkey 서버로 처리 결과를 조회한 후 서비스 서버의 사용자 정보와 동기화 하는데 사용한다.

// callback.jsp 예제 ... // passkey 트랜잭션 결과조회 API를 사용한 결과 조회 String txId = request.getParameter("svcTxId"); String type = request.getParameter("type"); String accessToken = ACCESS_TOKEN; URL url = new URL("https://svc.passkeys.kr/webauthn/api/v1/clients/check-tx"); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); ... String userHandle = resJson.getString("userHandle"); ... // userHandle을 서비스의 사용자 DB의 ID와 연동 ...

QR코드를 통한 진행

options.qrTokenClassifier()에서 QR토큰을 사용하는 장치로 판단되면 register() 호출시 Passkey 인증자(Authenticator) 사용 API를 호출 하는 대신 자동으로 QR토큰이 발급되며 options.qrCodeHandler()가 호출된다.
발급된 QR토큰이 처리되거나 취소 또는 만료 될때까지 QR토큰을 검사하며 처리가 완료되면 register()나 assertion()호출의 결과로 리턴되거나 예외가 발생한다.

options.qrCodeHandler = function(qrCodeData, qrCodeCanvas) { // var qrCodeContainer = document.getElementById('qrcode'); qrCodeContainer.innerHTML = ''; var text = document.createElement('h5'); text.innerText = qrCodeData; qrCodeContainer.appendChild(text); qrCodeContainer.appendChild(qrCodeCanvas); },

QR코드 절차 취소

QR토큰이 생성되어 QR코드가 표시되면 QR토큰이 만료(정상처리/취소/오류)될 때까지 options.qrTokenCheckInterval 주기마다 자동으로 QR토큰 상태를 확인한다. 이 때 사용자가 직접 취소할 수 있는 수단을 제공하고자 한다면 cancelQRToken()을 사용할 수 있다.

var options = { passkeyServiceRoot: 'https://svc.passkeys.kr/webauthn/api/v1', clientId: 'service1', origin: 'your-service-domain.com', email: 'user1@test.com', ... }; var passkeysClient = new WizveraPasskeys(options); passkeysClient.cancelQRToken({}).then(function(res) { // 취소되었음 document.getElementById('qrcode').innerHTML = ''; }).catch(function(error) { errorHandler(error); });

Autofill UI 지원

브라우저가 Passkey의 Autofill UI를 지원하면 브라우저의 자동완성 기능을 통해서 Passkey 인증을 할 수 있다. 해당 도메인에 이미 등록된 패스키의 인증을 브라우저의 자동완성 기능을 통하여 사용한다.

해당 기능을 활성화 하기 위해서는 ID 입력창의 <input>태그에 아래와 같이 autocomplete="username webauthn" 속성이 들어가야 한다.

<div> <label for="username">Username:</label> <input name="username" id="loginform.username" autocomplete="username webauthn"> </div>

위와 같이 <input> 태그에 해당 속성을 설정 후 아래 예제 enableAutofillUI() 함수를 통하여 인증 할 수 있다.

passkeysClient.isSupportAutofillUI().then(function(isAvailable) { if(isAvailable) { enableConditionalUI(); } }); ... function enableConditionalUI() { alert('conditional UI를 사용하면 assertion 버튼은 사용할수 없습니다.'); document.getElementById('assertionBtn').disabled = true; passkeysClient.enableAutofillUI({}).then(function(result) { writeLog(JSON.stringify(result, null, 2)); processLogin(result); }).catch(function(error) { errorHandler(error); document.getElementById('assertionBtn').disabled = false; }); }

주의사항

예외처리

모든 함수이 호출은 정상처리 외에 예외를 발생한다. 각 예외는 종류에 따라 name으로 구분되며 ServiceError는 별도의 오류코드를 가진다.

error.name에 따른 분류

name 설명
NotSupportError Passkey가 지원되지 않는 브라우저
InvalidOptionsError 함수 호출에 필요한 필수 파라미터 누락
ServerError 서버에서 오류(status 200이 아닌 에러 등) 발생
GeneralPasskeyError browser passkey api 오류(Webauthn API DomError)
AbortOperationError Passkey 인증 취소
ExcludedCredentialError 이미 서버에 등록된 Passkey가 재등록 시도됨
ServiceError Passkey 서비스 에러(세부 코드로 구분)

ServiceError의 code 분류

code 설명 비고
1014 등록되지 않은 사용자입니다. email인증이 필요합니다. 등록되지 않은 사용자 email로 register()호출시 발생
1005 등록되지 않은 사용자입니다. email로 인증코드가 전송되었습니다. checkUser() 호출시 미등록 사용자시 발생
1006 유효하지 않은 인증코드 입니다. verifyAuthCode() 호출시 잘못된 인증코드시 발생
1007 등록되지 않은 사용자입니다. requestAuthCode() 호출시 미등록 사용자시 발생

Client Web API Reference

WizveraPasskeys class static objects

WizveraPasskeys.QRTokenStatus = { "REQUESTED": "requested", "COMPLETED": "completed", "CANCELED": "canceled" }; WizveraPasskeys.QRTokenType = { "REGISTER": 1, "ASSERTION": 2 }

WizveraPasskeys class static methods

WizveraPasskey class instance methods

Sample HTML

<!DOCTYPE html> <html lang="ko"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>Wizvera Passkeys</title> <style> #txlog { width: 90%; height: 400px; } </style> <script src="https://svc.passkeys.kr/client/js/wizvera-passkeys.js"></script> </head> <body> <script type="text/javascript"> var PasskeyServiceRoot = 'https://svc.passkeys.kr/webauthn/api/v1'; var PasskeyClientId = 'localhost'; var PasskeyOrigin = window.location.hostname; var QRTokenProcessURL = window.location.origin + '/qr-pass.html'; var passkeysClient = undefined; var initWithQR = false; function initializeClient(withQR) { var clientInfo = getClientInfo(withQR); if(!clientInfo) return; var options = { // 패스키 서버 API 서비스 주소 passkeyServiceRoot: PasskeyServiceRoot, // 등록된 서비스 ID clientId: clientInfo.clientId, // 패스키 지원 origin origin: clientInfo.clientOrigin, // 사용자 email //email: clientInfo.email, // 사용자 전화번호 //phoneNumber: clientInfo.phoneNumber, // qr토큰 확인 시간(msec) qrTokenCheckInterval: 3000, // 외부 QR코드 생성 라이브러리 사용여부 useExternalQRCodeLibrary: clientInfo.useExternalQRCode, // QR인증을 통한 타기기로의 인증이 필요한 경우를 판별하는 함수를 override해야 할 필요가 있는 경우 설정 qrTokenClassifier: function() { return clientInfo.useQRToken; }, // QR코드로 표시할 URL 주소 qrTokenProcessURL: QRTokenProcessURL, // QR코드가 생성되면 호출되는 함수, useExternalQRCodeLibrary가 true면 qrCodeCanvas는 undefined qrCodeHandler: function(qrCodeData, qrCodeCanvas) { console.log(qrCodeData); var qrCodeContainer = document.getElementById('qrcode'); qrCodeContainer.innerHTML = ''; var text = document.createElement('h5'); text.innerText = qrCodeData; qrCodeContainer.appendChild(text); // useExternalQRCodeLibrary true면 qrCodeCanvas는 undefined가 리턴됨 if(clientInfo.useExternalQRCode !== true) { qrCodeContainer.appendChild(qrCodeCanvas); } }, }; if(withQR) options.qrToken = clientInfo.qrToken; try { passkeysClient = new WizveraPasskeys(options); // 브라우저가 Conditional Mediation을 지원하면 활성화 함 // https://passkeys.dev/docs/reference/terms/#autofill-ui passkeysClient.isSupportAutofillUI().then(function(isAvailable) { if(isAvailable) { enableConditionalUI(); } }); } catch(error) { errorHandler(error); } initWithQR = (withQR === true); } function register() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); var phoneNumber = document.getElementById('phoneNumber').value.trim(); if(!email) { alert('input email'); return; } passkeysClient.register({ email: email, phoneNumber: phoneNumber }).then(function(result) { writeLog(JSON.stringify(result, null, 2)); processSignup(result); }).catch(function(error) { errorHandler(error); }); } function assertion() { if(!passkeysClient) { alert('initialize first'); return; } var userHandle = document.getElementById('userHandle').value.trim(); passkeysClient.assertion({ userHandle: userHandle }).then(function(result) { writeLog(JSON.stringify(result, null, 2)); processLogin(result); }).catch(function(error) { errorHandler(error); }); } function processLogin(result) { // TODO: passkey 서명검증 후 처리 // result.callbackURL 호출 document.getElementById('assertionBtn').disabled = false; } function processSignup(result) { // TODO: passkey 등록완료 후 처리 // result.callbackURL 호출 } function enableConditionalUI() { alert('conditional UI를 사용하면 assertion 버튼은 사용할수 없습니다.'); document.getElementById('assertionBtn').disabled = true; passkeysClient.enableAutofillUI({}).then(function(result) { writeLog(JSON.stringify(result, null, 2)); processLogin(result); }).catch(function(error) { errorHandler(error); document.getElementById('assertionBtn').disabled = false; }); } function requestCreateOptions() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); var phoneNumber = document.getElementById('phoneNumber').value.trim(); if(!email) { alert('input email'); return; } passkeysClient.getPublicKeyCredentialCreationOptions({ email: email, phoneNumber: phoneNumber }).then(function(creationOptions) { writeLog(JSON.stringify(creationOptions, null, 2)); }).catch(function(error) { errorHandler(error); }); } function requestRequestOptions() { if(!passkeysClient) { alert('initialize first'); return; } passkeysClient.getPublicKeyCredentialRequestOptions({}).then(function(assertionOptions) { writeLog(JSON.stringify(assertionOptions, null, 2)); }).catch(function(error) { errorHandler(error); }); } function cancelQRToken() { if(!passkeysClient) { alert('initialize first'); return; } passkeysClient.cancelQRToken({}).then(function(res) { //writeLog(JSON.stringify(res, null, 2)); document.getElementById('qrcode').innerHTML = ''; }).catch(function(error) { errorHandler(error); }); } function checkEmail() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); if(!email) { alert('input email'); return; } passkeysClient.checkUser({ email: email }).then(function(res) { // 등록된 email이면 res는 항상 true를 리턴함 //writeLog('JSON.stringify(res, null, 2)'); writeLog('등록된 사용자입니다.'); }).catch(function(error) { errorHandler(error); }); } function requestCode() { if(!passkeysClient) { alert('initialize first'); return; } var email = document.getElementById('email').value.trim(); if(!email) { alert('input email'); return; } passkeysClient.requestAuthCode({ email: email }).then(function(res) { //writeLog(JSON.stringify(res, null, 2)); writeLog('등록된 사용자입니다. email로 인증코드가 전송되었습니다.'); }).catch(function(error) { errorHandler(error); }); } function checkCode() { if(!passkeysClient) { alert('initialize first'); return; } var authCode = document.getElementById('authCode').value.trim(); if(!authCode) { alert('input authCode'); return; } passkeysClient.verifyAuthCode({ authCode: authCode }).then(function(res) { // 인증코드가 확인되면 res는 항상 true를 리턴함. //writeLog(JSON.stringify(res, null, 2)); writeLog('인증코드가 확인되었습니다.'); }).catch(function(error) { errorHandler(error); }); } function getClientInfo(withQR) { var clientId = document.getElementById('clientId').value.trim(); var clientOrigin = document.getElementById('clientOrigin').value.trim(); var email = document.getElementById('email').value.trim(); var phoneNumber = document.getElementById('phoneNumber').value.trim(); var qrToken = document.getElementById('qrToken').value.trim(); var useQRToken = document.getElementById('useQRToken').value === 'true'; var useExternalQRCode = document.getElementById('useExternalQRCode').value === 'true'; var userHandle = document.getElementById('userHandle').value.trim(); if(!clientId) { alert('input clientId'); return; } if(!clientOrigin) { alert('input clientOrigin'); return; } /* if(!email && !userHandle) { alert('input email or userHandle'); return; } */ if(withQR && !qrToken) { alert('input qr-token'); return; } return { clientId: clientId, clientOrigin: clientOrigin, email: email, phoneNumber: phoneNumber, qrToken: qrToken, useQRToken: useQRToken, useExternalQRCode: useExternalQRCode, userHandle: userHandle }; } function writeLog(msg) { document.getElementById('txlog').textContent = msg; } function appendLog(msg, nl) { document.getElementById('txlog').textContent += ((nl ? "\n" : "") + msg); } function errorLog(error) { console.error(error); var message = (error.name === 'ServiceError') ? '[' + error.code + '] ' + error.message : error.message; writeLog('[' + error.name + ']' + message); } function errorHandler(error) { errorLog(error); var name = error.name; var message = error.message; var code = (name === 'ServiceError') ? error.code : undefined; var description = ''; if(name === 'NotSupportError') { description = '[' + name + '] Passkey가 지원되지 않는 브라우저입니다.\n\n' + message; } else if(name === 'InvalidOptionsError') { description = '[' + name + '] 필수 파라미터 누락: \n\n' + message; } else if(name === 'ServerError') { description = '[' + name + '] 서버에서 오류가 발생하였습니다: \n\n' + message; } else if(name === 'GeneralPasskeyError') { description = '[' + name + '] Passkey API 오류: \n\n' + message; } else if(name === 'AbortOperationError') { description = '[' + name + '] Passkey 인증이 취소되었습니다.: \n\n' + message; } else if(name === 'ExcludedCredentialError') { description = '[' + name + '] 이미 서버에 등록된 Passkey입니다. : \n\n' + message; } else if(name === 'ServiceError') { if(code === 1014) { description = '[' + name + '][' + code + '] 등록되지 않은 사용자입니다. email 인증을 해주세요. : \n\n' + message; } else if(code === 1005) { description = '[' + name + '][' + code + '] 등록되지 않은 사용자입니다. email로 인증코드가 전송되었습니다. : \n\n' + message; } else if(code === 1007) { description = '[' + name + '][' + code + '] 등록되지 않은 사용자입니다. : \n\n' + message; } else if(code === 1006) { description = '[' + name + '][' + code + '] 유효하지 않은 인증코드입니다. : \n\n' + message; } else { description = '[' + name + '][' + code + '] ' + message; } } else { description = '[' + name + '] ' + message; } alert(description); } window.addEventListener('DOMContentLoaded', function() { document.getElementById('clientId').value = PasskeyClientId; document.getElementById('clientOrigin').value = PasskeyOrigin; try { // throws NotSupportError if not supported WizveraPasskeys.IsSupportPasskey(); } catch(error) { errorHandler(error); } }); </script> <h4>Client & User Info</h4> clientId: <input type="text" id="clientId"><br/> origin: <input type="text" id="clientOrigin"><br/> email: <input type="text" id="email" autocomplete="username webauthn"><br/> phoneNumber: <input type="text" id="phoneNumber" disabled><br/> -<br/> userHandle: <input type="text" id="userHandle"><br/> -<br/> auth-code: <input type="text" id="authCode"><br/> -<br/> qr-token: <input type="text" id="qrToken"><br/> <hr/> <h4>Passkey API Test</h4> authenticate with a passkey on another device using QR code: <select id="useQRToken"> <option value="false" selected>false</option> <option value="true">true</option> </select><br/> use external qrcode library: <select id="useExternalQRCode"> <option value="false" selected>false</option> <option value="true">true</option> </select><br/> -<br/> initialize: <button onclick="initializeClient()">initialize client</button><br/> initialize with QRToken: <button onclick="initializeClient(true)">initialize client(QRToken)</button><br/> register: <button onclick="register()">register</button><br/> assertion: <button onclick="assertion()" id="assertionBtn">assertion</button><br/> -<br/> use conditional UI: <button onclick="enableConditionalUI()" id="conditionalUIBtn">ConditionalMediation(Conditional UI)</button> <hr/> <h4>QRCode</h4> <div id="qrcode"></div> <hr/> <h4>email & auth-code validation</h4> check email: <button onclick="checkEmail()">check email</button><br/> validate auth-code: <button onclick="checkCode()">validate auth-code</button><br/> -<br/> request auth-code for registered email: <button onclick="requestCode()">request auth-code</button><br/> <hr/> <h4>etc api test</h4> request create options: <button onclick="requestCreateOptions()">/passkey/create</button><br/> request request options: <button onclick="requestRequestOptions()">/passkey/request</button><br/> -<br/> cancel in progress QRToken: <button onclick="cancelQRToken()">cancel QRToken</button> <hr/> <textarea id="txlog" readonly></textarea> </body> </html>