打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
一起來了解 Web Authentication

前言

在今年年初的時候,W3C 正式將 Web Authentication (WebAuthn) 列入正式標準,開發者可以透過 API 啟動 FIDO 2 驗證,讓使用者透過行動裝置、指紋辨識或硬體安全金鑰來登入帳號,不需輸入密碼!

雖然目前支援度還不高,但既然已成為標準,沒理由不來認識一下何謂 Web Authentication,以及我們可以如何使用。今天就一起來了解我們能如何使用 Web Authentication,增加網站安全性,並免除密碼帶給我們的諸多麻煩。

先回憶一下我們平常的註冊登入方式

不管是手機 application 或是網站,只要是需要登入操作的,勢必都會提供一個頁面讓你註冊帳號與密碼,接著你才能拿著這組帳密登入進行使用。這是我們習以為常的使用方式,但隨之而來的問題大家也是見怪不怪:

  • 每個網站都要想一組新密碼,若用太固定的 pattern 又容易被猜到。

  • 太複雜的密碼容易忘記。

  • 自己要防範釣魚網站。

  • 需要擔心該網站被入侵,密碼遭偷竊。

而大約有三成左右危機意識較高的使用者會啟用 2FA (2 factor authentication),但這何嘗不是為使用上帶來更多不便?

因此 WebAuthn 就是希望來解決這些問題,為我們使用者提供一個除了密碼登入以外的選項。

再來說明一下基本觀念

聽到所謂免密碼登入,應該很容易聯想到我們透過 SSH 連進遠端 Server 時,大多也會設定免密碼登入,透過的是 Public-key cryptography 或又稱 asymmetric cryptography,也就是利用產生一組非對稱的加密金鑰:private keypublic key,又可稱作 credential,讓使用者自己保管 private key,而服務端利用 public key 來驗證使用者的身份。

WebAuthn 也是利用相同的加密原理來達成免密碼的 Authentication。

進入 API 細節前,我們科普一下,Web Authentication 與 FIDO

其實很多人會誤解,以為 WebAuthn 指的就是上述利用非對稱加密來達到 authentication 的技術,但實際上,真正規範這項技術的是 FIDO2,FIDO2 是 FIDO Alliance 的最新規範,裡面包含了一系列技術,讓使用者能透過一些普通的設備,輕易的與線上服務進行 authentication,無論是 Mobile 或是 Desktop。

FIDO2 規範包含了 WebAuthn 與 Client-to-Authenticator Protocol (CTAP)


ref: fidoalliance.org

也就是說,WebAuthn 其實是 FIDO2 的一部分,用來規範標準的 Web API 讓瀏覽器實作。

目前支援的瀏覽器有 Chrome, Firefox, Edge 與 Safari preview 版。另外也可以再多參考 Duo 的文章

我們稍微解釋一下上圖:

當使用者像應用程序要求註冊或登入時,使用 FIDO2 的服務端 (RP App Server) 會告知瀏覽器說,我們現在要用 FIDO Authentication,而瀏覽器則會使用 WebAuthn API,透過 CTAP protocol 來與 Authenticator 溝通,存取到需要的資訊並產生 public key 傳回給服務端。

這邊出現兩個新名詞:RP app server 與 Authenticator。

所謂的 RP (Relying Party),描寫的就是負責註冊與認證使用者的組織或是服務,基本上我覺得就是你的伺服器。

而 Authenticator 就是負責產生 credential 的軟硬體,例如:TouchID, YubiKey 等等。TouchID 這種存在於設備內部的,屬於 'platform' type,而 Yubikey 這類外部硬體設施則屬於 'cross-platform' type。

Web Authentication 的主要特性:

  • 強健的安全體系:Authentication 有安全性硬體作為支援,來儲存私鑰與進行 Web Authentication 需要的加密運算。

  • 效用受到作用域規範:產生的 Credential keypair 是綁定在 origin 上的,意思是說,註冊在 'techbridge.com' 的 keypair 是不能用在 'bad-techbridge.com' 的網域上頭,消除了釣魚攻擊的危險。

  • 受認證的(Attested):上面提到負責產生 credential 的 Authenticators 可以提供 certificate 來幫助服務端驗證拿到的 public key 是由可信任的 authenticators 發出,而非有害的來源。

Web Authentication API - Registering

接著我們可以開始來看看如何使用 Web Authentication API 來註冊使用者 (參考至 Duo 的 webauthn.guide

navigator.credentials.create()

首先,Server 利用 navigator.credentials.create() 來產生 client 的 credential

const credential = await navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions});

publicKeyCredentialCreationOptions 中有一堆 required 與 optional 的欄位需要填寫:

// should generate from serverconst challenge = new Uint8Array(32);window.crypto.getRandomValues(challenge);const userID = 'Kosv9fPtkDoh4Oz7Yq/pVgWHS8HhdlCto5cR0aBoVMw=';const id = Uint8Array.from(window.atob(userID), c=>c.charCodeAt(0));const publicKeyCredentialCreationOptions = {    challenge,    rp: {        name: 'Tech Bridge',        id: 'techbridge.inc',    },    user: {        id,        name: 'arvin@techbridge.cc',        displayName: 'Arvin',    },    pubKeyCredParams: [{alg: -7, type: 'public-key'}],    authenticatorSelection: {        authenticatorAttachment: 'platform',    },    timeout: 60000,    attestation: 'direct'};

challenge:challenge 是由 server 產生的一個 buffer,裡面含有一串隨機加密過的 bytes,用來防止 'replay attacks'。這邊單純 demo,用 Unit8Arraywindow.crypto.getRandomValues 做個示範。

rp:先前解釋過,代表 'relying party',指的就是註冊與認證使用者的網站。其中,id 一定要是目前網站的 domain 的 subset,像是如果是從 techbridge.cc 觸發 WebAuthn 認證,那這邊的 id 就得填寫 techbridge.cc

user:就是目前想要註冊的使用者的資訊,這邊的 id 很重要,authenticator 會用這個 id 來與 credential 做連結,這樣之後才能透過一樣的 id 與一樣的 autenticator 去認證你是同個 user。而這資訊可能會被存在 authenticator 中,依據不同載具與實作會有不同,但是因為有這可能性存在,所以一般不建議 id 內藏有可以認出你本身的相關個人資訊,像是生日等等的。

pubKeyCredParams:這個 array 用來描述 Server 支援哪些 types 的 public key (signing algorithms) 。目前 FIDO2 servers 授權支援 RS1、RS256 與 ES256。 alg 的數字對應可以從 COSE 去找。

authenticatorSelection (optional):可以用來限制 Relying parties 支援的 authenticators 種類,像這邊我指定 platform,就是 Touch ID 之類內建的 authenticators。

timeout (optional):以 milliseconds 為單位,表示說如果使用者在這段 timeout 時間內沒有回應 prompt 的話,就會回傳認證失敗。

attestation (optional):attestation data 是從 authenticator 回傳的內容,server 可以用這個選項來決定要跟 authenticators 拿多少資訊,有三種選項:noneindirectdirectnone 指的是 server 不在意 attestation 資料;indirect 表示願意只拿匿名的 attestation 資料;而 direct 代表要求 authenticator 回傳所有 attestation data。

看了一些程式碼,來嘗試一下吧

現在打開瀏覽器的 console,把上面那段 code 複製上去,然後你會發現什麼事都沒發生?!

因為上面那段 code 當中,我們在 rp 中的 id 有指定 techbridge.inc,而這並非你目前所在的 domain,因此無法啟用 WebAuthn。

修改一下,將 rp 中的 id 移除,再試一次:

就可以 credentials 了!

另外,因為我們在 authenticatorSelection.authenticatorAttachment 中有指定 platform,所以當我們一請求 credential 後,Chrome 會直接跳出 prompt 要求使用 TouchID(上面出現 codepen.io 是因為我在 codepen.io 的頁面 console 測試:

但如果今天你沒有指定,或是指定為 cross-platform,則會顯示如下兩種 prompt:

cross-platform

會要求你插入實體 usb key。

不指定:

你有兩種選項可以選擇,使用 TouchID 或是實體 usb key。

Parsing 與 Validating 註冊資料

從剛剛的範例可以看到,呼叫 navigator.credentials.create 後會回傳一個 credential 物件:

console.log(credential);PublicKeyCredential { id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...', rawId: ArrayBuffer(59), response: AuthenticatorAttestationResponse { clientDataJSON: ArrayBuffer(121), attestationObject: ArrayBuffer(306), }, type: 'public-key'}

其中:

id:代表產生的 credential,在對 user 進行 authentication 時,用此 id 來 identify user credential,是 base64-encoded string。

rawId:同上,只是是 binary form。

response.clientDataJSON:是瀏覽器產生的一組資料,包含 origin、challenge 等等,這個資料重要在於可以用來防止 phishing attemp。此外,內容是由 authenticator 加密過的。

response.attestationObject:包含了 credential public key、optional attestation certificate 與其他 metadata,用 CBOR encoded 過的 binary data。

拿到 PublicKeyCredential 後,就可以送到 Server 端去進行驗證與後續動作。

WebAuthn 規範中有描述了一系列驗證 registration data 的程序,而實作部分則與你用什麼語言有所不同,可以參考 Duo Labs 的 GoPython 版本。

這邊以 JavaScript 為例說明:

首先,Server 端需要先解析 clientDataJSON:

// decode the clientDataJSON into a utf-8 stringconst utf8Decoder = new TextDecoder('utf-8');const decodedClientData = utf8Decoder.decode(    credential.response.clientDataJSON)// parse the string as an objectconst clientDataObj = JSON.parse(decodedClientData);console.log(clientDataObj){    challenge: 'ZVTQWf9y7JkEjKFH-iWiKL4FTtTrQJwhYt2kiJQlcM8'    origin: 'https://codepen.io' // again, 因為我在 codepen.io 的頁面 console 測試,所以 origin 才會是這個    type: 'webauthn.create'}

這邊我們拿出 challenge、origin 與 type 來驗證,challenge 應該要與當初 Server 產生的一致、origin 要正確,且 type 要確定為 create,才能代表是在註冊使用者。如此來避免釣魚與 replay attacks。

接著,處理 attestationObject,由於是由 CBOR encode,所以需要額外找 lib 來 decode:

// 需要找個 CBOR lib 來 decodeconst decodedAttestationObj = CBOR.decode(credential.response.attestationObject);console.log(decodedAttestationObj);{ authData: Uint8Array(196), fmt: 'fido-u2f', attStmt: { sig: Uint8Array(70), x5c: Array(1), },}

解碼出來後的 attestationObject 含有幾項資訊:

  • authData:authData 這個 byte array 包含著所有 registration event 的 metadata,以及 public key。

  • fmt:這個是包含著 attestation 的 format,如果你在 create credentials 時有要求 Authenticators 提供 attestation data,那 server 可以從這個欄位知道該如何 parse 與 validate attestation data。

  • attStmt:這就是要求來的 attestation data,根據 fmt 的不同會有不同的結構,以這邊範例為例,我們拿到的是一個 signature 與 x5c certificate,servers 可以用這資料來驗證 publickey 是不是來自預期的 authenticator,或是根據 authenticator 的資訊而 reject authenticate (像是覺得不能信任該 certifacate,等等)

最後可以從 authData 中取得更多資料:credentialId、publicKeyBytes、publicKeyObject 等等:

const {authData} = decodedAttestationObject;// get the credential IDconst credentialId = authData.slice(55, credentialIdLength);// get the public key objectconst publicKeyBytes = authData.slice(55 + credentialIdLength);// the publicKeyBytes are encoded again as CBORconst publicKeyObject = CBOR.decode(publicKeyBytes.buffer);

整個 Validation process 完成後,Server 就能將 publicKeyBytes 與 credentialId 存進資料庫,與使用者關聯起來。

到這邊為止,我們走完了 Registering 的流程。而剛剛都是直接在 console 貼入程式碼,如果想看稍微”真實“一點的範例,可以到 https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/makeCredExample.html 這個網址玩玩看,點選 button 註冊的流程。

接著,再努力一下,看看 Authenticating,也就是登入時要走的步驟吧!

Web Authentication API - Authenticating

Authenticated 的過程中,使用者會用其持有的 private key 簽上一個 signature 到 assertion 上頭,並傳給 server,server 則利用 public key 來驗證該 signature。

navigator.credentials.get()

Authentication 的過程在於使用者要證明他們擁有註冊時所提交的 key pair 中的 private key。證明方法為利用 navigator.credentials.get() 取得註冊時的 credential,並附上 signature。

const credential = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions});
const challenge = new Uint8Array(32);window.crypto.getRandomValues(challenge);const publicKeyCredentialRequestOptions = {    challenge,    allowCredentials: [{        id: credentialId, // from registration        type: 'public-key',        transports: ['usb', 'ble', 'nfc'],    }],    timeout: 60000,}const assertion = await navigator.credentials.get({    publicKey: publicKeyCredentialRequestOptions});

跟註冊時比較不同的是 allowCredentials,這欄位是 Servers 端想要瀏覽器提供的 credentials 資訊,用來認證使用者,其中 id 為註冊時獲取的 credentialId。此外也能指定要用哪種方式傳輸 credentials (usb、bluetooth 或 NFC)。你可以填入多個 credentialId,authenticator 會找出他認識的來使用。

navigator.credentials.get 取得的 assertion 也是一個 PublicKeyCredential object,跟註冊時的差別在於,這次我們拿到的多了 signature,少了 public key。

console.log(assertion);PublicKeyCredential { id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...', rawId: ArrayBuffer(59), response: AuthenticatorAssertionResponse { authenticatorData: ArrayBuffer(191), clientDataJSON: ArrayBuffer(118), signature: ArrayBuffer(70), userHandle: ArrayBuffer(10), }, type: 'public-key'}

基本結構都差不多,多出來的 signature 是由 private key 與此 credential 所產生,在 server 端可以用 public key 來驗證此 signature。

userHandle 則是 authenticator 提供的使用者 id,也是註冊時使用的那組。在 Server 端可以用來關聯使用者。

Parsing 與 Validating 登入資料

拿到 assertion 後,可以送往 server 進行驗證,server 會用註冊時的 public key 來驗證 signature。

一樣,Server 端實作有各種語言版本:GoPython,等等。

這邊簡單舉例:

首先,從資料庫中取得 credential。接著利用 public key 和 client 傳來的 signature 與 authenticator data 和 SHA-256 hash 過的 cliendDataJSON 做驗證。

const storedCredential = await getCredentialFromDatabase(userHandle, credentialId);const signedData = (authenticatorDataBytes + hashedClientDataJSON);const signatureIsValid = storedCredential.publicKey.verify(signature, signedData);if (signatureIsValid) {    return 'Nice! User is authenticated!';} else {    return 'Oops, verification failed.'}

這樣我們就把一段無密碼登入的註冊與登入流程都走完了!一樣,可以到 https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/PasswordlessExample.html 來玩玩看實際一點的範例。

結論與更多資源

今天花了點時間把 Web Authentication 的概念與大致的實作方式瞭解了一番,雖然才剛成為標準,離全部瀏覽器都支援還需要一點時間,但最新版的瀏覽器幾乎都已支援(至少桌機版),想必不久的未來,passwordless 的登入會是一種趨勢。

這邊再多提供幾個網站資源供大家參考,裡面有更為豐富的範例程式,以及詳細的 API 與情境解說:

  1. Duo blog - Duo 是 Cisco 旗下的公司,主要發展與 Authentication 與 security 相關的產品,也因此針對 web authentication 有不少的文章教學與介紹。

  2. FIDO2WebAuthnSeries - 這是我在找資料過程中發現的範例 github,裡面有多種情境的 source code 與 demo,推薦大家去玩玩看,看看程式碼,想必會更加了解。作者也有在 Medium 上寫了一篇長文介紹,更詳細的說明各種情境的 Web authentication 應用會是如何的流程,而我們的程式碼又該如何修改。如果覺得這篇文章內的範例與說明不夠清楚,可以到他的 medium 看看。

資料來源

  1. webauthn.guide

  2. Introduction to WebAuthn API

  3. FIDO2WebAuthnSeries

  4. webauthn.io

  5. Web Authentication API - MDN

關於作者:
@arvinh 前端攻城獅,熱愛數據分析和資訊視覺化

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
一名老師只用了紙屑和垃圾桶,便教會學生一個最重要的人生智慧。
h3c CONSOLE 配置
Tonido 把你的電腦打造為雲端服務,自架免空、檔案共享或部落格
360doc个人图书馆
FileZilla Server 的使用!
Exchange Server 2013快速上手步驟TOP 6
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服