1. 빌드할 항목
비밀번호 기반 로그인을 지원하는 기본 웹 애플리케이션으로 시작합니다.
그런 다음 WebAuthn을 기반으로 보안 키를 통해 2단계 인증 지원을 추가합니다. 이를 위해 다음을 구현합니다.
- 사용자가 WebAuthn 사용자 인증 정보를 등록하는 방법.
- WebAuthn 사용자 인증 정보를 등록하는 사용자에게 2단계 인증(2단계 인증 절차)을 요청하는 2단계 인증 흐름
- 사용자 인증 정보 관리 인터페이스: 사용자가 사용자 인증 정보의 이름을 바꾸거나 삭제할 수 있는 사용자 인증 정보 목록입니다.
완성된 웹 앱을 살펴보고 사용해 보세요.
2. WebAuthn 정보
WebAuthn 기본사항
WebAuthn을 사용해야 하는 이유
피싱은 웹에서 엄청난 보안 문제를 야기합니다. 대부분의 계정 위반은 사이트에서 재사용되는 취약하거나 도용된 비밀번호를 활용합니다. 이 문제에 대한 업계의 총체적인 대응은 다단계 인증이지만, 단편화되어 있어서 대다수가 여전히 피싱을 적절히 해결할 수 없습니다.
Web Authentication API 또는 WebAuthn은 모든 웹 애플리케이션에서 사용할 수 있는 표준화된 피싱 방지 프로토콜입니다.
작동 원리
출처: webauthn.guide
WebAuthn을 사용하면 서버에서 비밀번호 대신 공개 키 암호화를 사용하여 사용자를 등록하고 인증할 수 있습니다. 웹사이트는 비공개-공개 키 쌍으로 구성된 사용자 인증 정보를 만들 수 있습니다.
- 비공개 키가 사용자 기기에 안전하게 저장됩니다.
- 보관을 위해 공개 키 및 무작위로 생성된 사용자 인증 정보 ID가 서버로 전송됩니다.
공개 키는 서버에서 사용자 ID를 증명하는 데 사용합니다. 해당 비공개 키 없이는 소용이 없기 때문에 비밀이 아닙니다.
이점
WebAuthn에는 다음과 같은 두 가지 주요 이점이 있습니다.
- 공유 비밀번호 없음: 서버에 보안 비밀이 저장되지 않습니다. 공개 키는 유용하지 않기 때문에 해커의 데이터베이스를 덜 매력적으로 만들 수 있습니다.
- 범위 사용자 인증 정보:
site.example
에 등록된 사용자 인증 정보는evil-site.example
에서 사용할 수 없습니다. 따라서 WebAuthn 피싱을 방지할 수 있습니다.
사용 사례
WebAuthn의 한 가지 사용 사례는 보안 키를 사용하는 2단계 인증입니다. 이는 특히 엔터프라이즈 웹 애플리케이션과 관련이 있을 수 있습니다.
브라우저 지원
이 게시물은 W3C 및 FIDO에서 작성했으며 Google, Mozilla, Microsoft, Yubico 및 다른 업체에서 참여합니다.
용어집
- OTP: 사용자를 등록하고 나중에 등록된 사용자 인증 정보의 소유권을 주장할 수 있는 소프트웨어 또는 하드웨어 항목입니다. 인증에는 다음과 같은 두 가지 유형이 있습니다.
- 로밍 OTP: 사용자가 로그인하려는 모든 기기에서 사용할 수 있는 OTP입니다. 예: USB 보안 키, 스마트폰
- 플랫폼 인증자: 사용자 기기에 내장된 인증자입니다. 예: Apple의 Touch ID입니다.
- 사용자 인증 정보: 비공개-공개 키 쌍
- 신뢰 당사자: 사용자를 인증하려는 웹사이트의 서버
- FIDO 서버: 인증에 사용되는 서버 FIDO는 FIDO 얼라이언스에서 개발한 프로토콜 모음으로, WebAuthn이 여기에 해당합니다.
이 워크숍에서는 로밍 OTP를 사용합니다.
3. 시작하기 전에
필요한 항목
이 Codelab을 완료하려면 다음이 필요합니다.
- WebAuthn에 관한 기본적인 이해
- 자바스크립트 및 HTML에 대한 기본 지식
- WebAuthn을 지원하는 최신 브라우저
- U2F 호환 보안 키.
다음 중 하나를 보안 키로 사용할 수 있습니다.
- Chrome을 실행하는 Android>=7 (Nougat)을 사용하는 Android 휴대전화 이 경우 블루투스가 지원되는 Windows, macOS 또는 Chrome OS 머신도 필요합니다.
- YubiKey와 같은 USB 키
출처: https://www.yubico.com/products/security-key/
학습할 내용
학습할 내용 ✅
- WebAuthn 인증의 2단계 인증 방법으로 보안 키를 등록하고 사용하는 방법
- 이 프로세스를 사용자 친화적으로 만드는 방법
학습에서 승리했습니다. 😈
- 인증에 사용하는 서버인 FIDO 서버를 빌드하는 방법 일반적으로 웹 애플리케이션이나 사이트 개발자는 기존 FIDO 서버 구현을 활용하기 때문에 문제가 되지 않습니다. 사용하는 서버 구현의 기능과 품질을 항상 확인해야 합니다. 이 Codelab에서 FIDO 서버는 SimpleWebAuthn을 사용합니다. 다른 옵션은 FIDO Alliance 공식 페이지를 참고하세요. 오픈소스 라이브러리는 webauthn.io 또는 AwesomeWebAuthn을 참고하세요.
면책조항
사용자가 로그인하려면 비밀번호를 입력해야 합니다. 하지만 이 Codelab에서는 편의상 비밀번호를 저장하거나 확인하지 않습니다. 실제 애플리케이션에서는 서버 측에서 올바른지 확인할 수 있습니다.
이 Codelab에서는 CSRF 검사, 세션 유효성 검사, 입력 완전 삭제와 같은 기본 보안 확인을 구현합니다. 그러나 무차별 대입 공격을 방지하기 위해 비밀번호에 대한 입력 제한은 없는 등 여러 보안 조치가 지원되지 않습니다. 비밀번호가 저장되지 않으므로 여기에서는 중요하지 않지만 이 코드를 프로덕션에서 그대로 사용하면 안 됩니다.
4. 인증자 설정
Android 휴대전화를 인증자로 사용하는 경우
- 데스크톱과 휴대전화 모두에서 Chrome이 최신 버전인지 확인합니다.
- 데스크톱과 휴대전화에서 모두 Chrome을 열고 이 워크숍에 사용할 프로필과 동일한 프로필(⏤)으로 로그인합니다.
- 데스크톱과 휴대전화에서 이 프로필의 동기화를 사용 설정합니다. 대신 chrome://settings/syncSetup을 사용하세요.
- 데스크톱과 휴대전화에서 블루투스를 사용 설정합니다.
- Chrome 데스크톱에 동일한 프로필로 로그인한 상태에서 webauthn.io를 엽니다.
- 간단한 사용자 이름을 입력합니다. 증명 유형 및 인증자 유형은 없음 및 지정되지 않음(기본값) 값으로 둡니다. 등록을 클릭합니다.
- 신원 확인을 요청하는 브라우저 창이 열립니다. 목록에서 휴대전화를 선택합니다.
- 휴대전화에 본인 확인이라는 알림이 표시됩니다. 탭합니다.
- 휴대전화에서 휴대전화의 PIN 코드를 입력하거나 지문 센서를 터치하라는 메시지가 표시됩니다. 비밀번호를 입력합니다.
- 데스크톱의 webauthn.io에 '성공' 표시기가 표시됩니다.
- 데스크톱의 webauthn.io에서 로그인 버튼을 클릭합니다.
- 다시 브라우저 창이 열리면 목록에서 내 휴대전화를 선택합니다.
- 휴대전화에서 팝업 알림을 탭한 후 PIN을 입력하거나 지문 센서를 터치하세요.
- webauthn.io가 로그인되어 있다고 알려줍니다. 휴대전화가 보안 키로 정상적으로 작동하고 있습니다. 워크숍이 모두 준비되었습니다.
USB 보안 키를 인증자로 사용하는 경우
- Chrome 데스크톱에서 webauthn.io를 엽니다.
- 간단한 사용자 이름을 입력합니다. 증명 유형 및 인증자 유형은 없음 및 지정되지 않음 (기본값) 값으로 둡니다. 등록을 클릭합니다.
- 신원 확인을 요청하는 브라우저 창이 열립니다. 목록에서 USB 보안 키를 선택합니다.
- 데스크톱에 보안 키를 삽입하고 터치합니다.
- 데스크톱의 webauthn.io에 '성공' 표시기가 표시됩니다.
- 데스크톱의 webauthn.io에서 로그인 버튼을 클릭합니다.
- 브라우저 창이 다시 열리고 목록에서 USB 보안 키를 선택합니다.
- 키를 터치합니다.
- Webauthn.io에서 로그인 상태를 확인해야 합니다. USB 보안 키가 제대로 작동하고 있습니다. 워크숍이 준비되었습니다.
5. 설정
이 Codelab에서는 코드를 자동으로 즉시 배포하는 온라인 코드 편집기인 Glitch를 사용합니다.
시작 코드 포크
시작 프로젝트를 엽니다.
리믹스 버튼을 클릭합니다.
이렇게 하면 시작 코드의 사본이 생성됩니다. 이제 수정할 코드가 있습니다. 포크 (Glquo에서 "remix"라고 함)는 이 Codelab의 모든 작업을 실행합니다.
시작 코드 탐색
방금 포크한 시작 코드를 살펴보세요.
libs
아래에 auth.js
라는 라이브러리가 이미 제공되어 있습니다. 서버 측 인증 로직을 처리하는 맞춤 라이브러리입니다. fido
라이브러리를 종속 항목으로 사용합니다.
6. 사용자 인증 정보 등록 구현
사용자 인증 정보 등록 구현
보안 키로 2단계 인증을 설정하기 위해 가장 먼저 필요한 것은 사용자가 사용자 인증 정보를 생성하도록 하는 것입니다.
먼저 클라이언트 측 코드에서 이 작업을 실행하는 함수를 추가해 보겠습니다.
public/auth.client.js
에는 아직 아무것도 하지 않는 registerCredential()
이라는 함수가 있습니다. 다음 코드를 파일에 추가합니다.
async function registerCredential() {
// Fetch the credential creation options from the backend
const credentialCreationOptionsFromServer = await _fetch(
"/auth/credential-options",
"POST"
);
// Decode the credential creation options
const credentialCreationOptions = decodeServerOptions(
credentialCreationOptionsFromServer
);
// Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
const credential = await navigator.credentials.create({
publicKey: {
...credentialCreationOptions,
}
});
// Encode the newly created credential to send it to the backend
const encodedCredential = encodeCredential(credential);
// Send the encoded credential to the backend for storage
return await _fetch("/auth/credential", "POST", encodedCredential);
}
이 함수는 이미 내보내졌습니다.
registerCredential
의 기능은 다음과 같습니다.
- 서버(
/auth/credential-options
)에서 사용자 인증 정보 생성 옵션을 가져옵니다. - 서버 옵션은 다시 인코딩되기 때문에 유틸리티 함수
decodeServerOptions
을 사용하여 디코딩합니다. - 웹 API
navigator.credential.create
를 호출하여 사용자 인증 정보를 생성합니다.navigator.credential.create
가 호출되면 브라우저가 인계받아 보안 키를 선택하라는 메시지가 사용자에게 표시됩니다. - 새로 만든 사용자 인증 정보를 디코딩합니다.
- 이 API는 인코딩된 사용자 인증 정보가 포함된
/auth/credential
에 요청을 보내 서버 측에 새 사용자 인증 정보를 등록합니다.
부수: 서버 코드 살펴보기
registerCredential()
는 서버를 2번 호출하므로 백엔드에서 어떤 일이 일어나는지 잠시 살펴보겠습니다.
사용자 인증 정보 생성 옵션
클라이언트에서 /auth/credential-options
요청을 보내면 서버에서 옵션 객체를 생성하여 클라이언트에 다시 전송합니다.
이후 이 객체는 클라이언트가 실제 사용자 인증 정보 생성 호출에서 사용됩니다.
navigator.credentials.create({
publicKey: {
// Options generated server-side
...credentialCreationOptions
// ...
}
그렇다면 이 credentialCreationOptions
의 정보는 이전 단계에서 구현한 클라이언트 측 registerCredential
에 사용된 것인가요?
router.post("/credential-options", ...)에서 서버 코드를 확인합니다.
모든 속성을 살펴보지는 않지만 fido2
라이브러리를 사용하여 생성되고 최종적으로 클라이언트에 반환된 서버 코드의 흥미로운 객체를 몇 가지 소개합니다.
rpName
및rpId
는 사용자를 등록하고 인증하는 조직을 설명합니다. WebAuthn에서 사용자 인증 정보는 특정 도메인으로 범위가 지정되므로 보안상의 이점이 있습니다. 여기서rpName
및rpId
는 사용자 인증 정보의 범위를 지정하는 데 사용됩니다. 예를 들어 유효한rpId
는 사이트의 호스트 이름입니다. 시작 프로젝트를 포크할 때 자동으로 업데이트되는 내용을 확인하세요. 🧘 ♀️excludeCredentials
은 사용자 인증 정보 목록입니다. 새 사용자 인증 정보를excludeCredentials
에 나열된 사용자 인증 정보 중 하나를 포함하는 인증자에서 만들 수 없습니다. Codelab에서excludeCredentials
는 이 사용자의 기존 사용자 인증 정보 목록입니다.user.id
를 사용하면 사용자가 만드는 각 사용자 인증 정보가 다른 인증자 (보안 키)에 상주하도록 보장합니다. 이 방법은 사용자가 여러 개의 사용자 인증 정보를 등록한 경우 여러 인증자 (보안 키)를 사용하게 되므로 하나의 보안 키를 분실하면 사용자가 계정에 액세스하지 못하게 되므로 주의해야 합니다.authenticatorSelection
는 웹 애플리케이션에서 허용할 인증자의 유형을 정의합니다.authenticatorSelection
를 자세히 살펴보겠습니다.residentKey: preferred
은 이 애플리케이션이 클라이언트 측 검색 가능한 사용자 인증 정보를 적용하지 않음을 의미합니다. 클라이언트 측 검색 가능한 사용자 인증 정보는 먼저 사용자를 식별하지 않고도 사용자를 인증할 수 있게 해주는 특수한 유형의 사용자 인증 정보입니다. 이 Codelab에서는 기본 구현에 중점을 두므로preferred
를 설정했습니다. 검색 가능한 사용자 인증 정보는 고급 흐름에 사용됩니다.requireResidentKey
는 WebAuthn v1과의 이전 버전과의 호환성을 위해서만 제공됩니다.userVerification: preferred
는 인증자가 사용자 확인을 지원하는 경우(예: 생체 인식 보안 키나 내장된 PIN 기능이 있는 키) 사용자 인증 정보를 생성할 때 인증자가 이를 요청하는 것을 의미합니다. 인증자가 기본 보안 키인 경우 사용자 인증을 요청하지 않습니다.
pubKeyCredParam
는 선호도에 따라 사용자 인증 정보의 원하는 암호화 속성을 설명합니다.
모든 옵션은 웹 애플리케이션에서 보안 모델을 위해 결정해야 합니다. 서버에서 이러한 옵션은 단일 authSettings
객체에 정의됨을 확인합니다.
당면 과제
또 다른 흥미로운 부분은 req.session.challenge = options.challenge;
입니다.
WebAuthn은 암호화 프로토콜이므로 공격자가 공격을 시도하여 인증을 재생성하거나 인증을 사용 설정할 비공개 키의 소유자가 아닌 경우 무작위로 공격을 시도하여 공격을 피해야 합니다.
이 문제를 완화하기 위해 챌린지가 서버에서 생성되고, 즉시 서명됩니다. 그런 다음 서명이 예상한 것과 비교됩니다. 이렇게 하면 사용자 인증 정보 생성 시 사용자가 비공개 키를 유지할 수 있습니다.
사용자 인증 정보 등록 코드
router.post("/credential" ...)에서 서버 코드를 확인합니다.
여기에서 사용자 인증 정보가 서버 측에 등록됩니다.
이제 어떻게 해야 할까요?
이 코드에서 가장 주목할 만한 비트 중 하나는 fido2.verifyAttestationResponse
를 통한 인증 호출입니다.
- 서명된 챌린지가 확인되고, 이는 생성 시 실제로 비공개 키를 유지한 사람이 사용자 인증 정보를 생성했음을 보장합니다.
- 출처에 바인딩된 신뢰 당사자 ID도 인증됩니다. 이렇게 하면 사용자 인증 정보가 이 웹 애플리케이션에 바인딩됩니다 (이 웹 애플리케이션만).
UI에 이 기능 추가
이제 사용자 인증 정보를 만드는 함수인 `registerCredential(),
이 준비되었으므로 사용자에게 제공할 수 있습니다.
이 작업은 일반적으로 인증 관리를 위한 위치이므로 계정 페이지에서 이 작업을 수행합니다.
account.html
마크업의 사용자 이름 아래에 레이아웃 클래스 class="flex-h-between"
가 있는 비어 있는 div
가 있습니다. 2FA 기능과 관련된 UI 요소에 이 div
를 사용합니다.
이 div를 추가합니다.
- '2단계 인증'으로 표시된 제목
- 사용자 인증 정보를 만드는 버튼
<div class="flex-h-between">
<h3>
Two-factor authentication
</h3>
<button class="create" id="registerButton" raised>
➕ Add a credential
</button>
</div>
이 div 아래에 나중에 필요한 사용자 인증 정보 div를 추가합니다.
<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>
account.html
인라인 스크립트에서 방금 만든 함수를 가져와 이 함수를 호출하는 함수 register
와 방금 만든 버튼에 연결된 이벤트 핸들러를 추가합니다.
// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);
// Register a credential
async function register() {
let user = {};
try {
const user = await registerCredential();
} catch (e) {
// Alert the user that something went wrong
if (Array.isArray(e)) {
alert(
// `msg` not `message`, this is the key's name as per the express validator API
`Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
);
} else {
alert(`Registration failed. ${e}`);
}
}
}
사용자가 볼 수 있는 사용자 인증 정보 표시
사용자 인증 정보를 만드는 기능이 추가되었으므로 이제 사용자가 추가한 사용자 인증 정보를 볼 방법이 필요합니다.
계정 페이지를 사용하면 됩니다.
account.html
에서 updateCredentialList()
라는 함수를 찾습니다.
백엔드에 호출하여 현재 로그인한 사용자의 등록된 모든 사용자 인증 정보를 가져오고 반환된 사용자 인증 정보를 표시하는 코드를 추가합니다.
// Update the list that displays credentials
async function updateCredentialList() {
// Fetch the latest credential list from the backend
const response = await _fetch('/auth/credentials', 'GET');
const credentials = response.credentials || [];
// Generate the credential list as HTML and pass remove/rename functions as args
const credentialListHtml = getCredentialListHtml(
credentials,
removeEl,
renameEl
);
// Display the list of credentials in the DOM
const list = document.querySelector('#credentials');
render(credentialListHtml, list);
}
지금은 removeEl
및 renameEl
에 신경 쓰지 않아도 됩니다. 이 Codelab의 뒷부분에서 자세히 알아봅니다.
인라인 스크립트 시작 부분의 account.html
내에서 updateCredentialList
호출을 1번 추가합니다. 이 호출에서는 사용자가 계정 페이지를 방문할 때 사용 가능한 사용자 인증 정보를 가져옵니다.
<script type="module">
// ... (imports)
// Initialize the credential list by updating it once on page load
updateCredentialList();
이제 registerCredential
가 성공적으로 완료되면 updateCredentialList
를 호출하여 목록에 새로 만든 사용자 인증 정보가 표시됩니다.
async function register() {
let user = {};
try {
// ...
} catch (e) {
// ...
}
// Refresh the credential list to display the new credential
await updateCredentialList();
}
직접 시험해 보세요. 👩 💻
사용자 인증 정보 등록을 완료했습니다. 이제 사용자가 보안 키 기반 사용자 인증 정보를 만들고 계정 페이지에서 시각화할 수 있습니다.
사용해 보기:
- 로그아웃을 클릭합니다.
- 모든 사용자 및 비밀번호로 로그인합니다. 앞서 언급했듯이 이 Codelab에서는 작업을 단순화하기 위해 비밀번호가 실제로 정확한지 검사하지 않습니다. 비어 있지 않은 비밀번호를 입력하세요.
- 계정 페이지에서 사용자 인증 정보 추가를 클릭합니다.
- 보안 키를 삽입하고 터치하라는 메시지가 표시됩니다. 잊지 말고 가치를 할당하세요.
- 사용자 인증 정보가 생성되면 계정 페이지에 사용자 인증 정보가 표시됩니다.
- 계정 페이지를 새로고침합니다. 사용자 인증 정보가 표시되어야 합니다.
- 사용 가능한 키가 2개 있는 경우 두 개의 보안 키를 사용자 인증 정보로 추가해 봅니다. 둘 다 표시되어야 합니다.
- 동일한 인증자 (키)로 사용자 인증 정보를 두 개 만들어 보세요. 지원되지 않는 것을 확인할 수 있습니다. 이는 의도적인 결과입니다. 백엔드에서
excludeCredentials
을 사용하기 때문입니다.
7. 2단계 인증 사용 설정
사용자가 사용자 인증 정보를 등록하거나 등록 취소할 수 있지만, 사용자 인증 정보는 표시되어서 아직 실제로 사용되지는 않았습니다.
이제 실제로 사용하고 2단계 인증을 설정할 차례입니다.
이 섹션에서는 웹 애플리케이션의 인증 흐름을 이 기본 흐름에서 변경합니다.
이 2단계 인증을 구매하는 방법은 다음과 같습니다.
2단계 인증 구현
먼저 필요한 기능을 추가하고 백엔드와의 통신을 구현하겠습니다. 다음 단계에서는 프런트엔드에 추가합니다.
여기서 구현해야 하는 작업은 사용자 인증 정보로 사용자를 인증하는 함수입니다.
public/auth.client.js
에서 빈 함수 authenticateTwoFactor
를 찾아 다음 코드에 추가합니다.
async function authenticateTwoFactor() {
// Fetch the 2F options from the backend
const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
// Decode them
const decodedOptions = decodeServerOptions(optionsFromServer);
// Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
const credential = await navigator.credentials.get({
publicKey: decodedOptions
});
// Encode the credential
const encodedCredential = encodeCredential(credential);
// Send it to the backend for verification
return await _fetch("/auth/authenticate-two-factor", "POST", {
credential: encodedCredential
});
}
이 함수는 이미 내보내져 있습니다. 다음 단계에서 필요합니다.
authenticateTwoFactor
의 기능은 다음과 같습니다.
- 서버에서 2단계 인증 옵션을 요청합니다. 이전에 본 사용자 인증 정보 생성 옵션과 마찬가지로 이는 서버에서 정의되며 웹 애플리케이션의 보안 모델에 따라 달라집니다. 자세한 내용은
router.post("/two-factors-options", ...
의 서버 코드를 자세히 알아보세요. navigator.credentials.get
를 호출하면 브라우저가 우선 조치되고 이전에 등록된 키를 삽입하고 터치하라는 메시지가 표시됩니다. 이렇게 하면 이 특정 2단계 인증 작업의 사용자 인증 정보가 선택됩니다.- 선택한 사용자 인증 정보가 가져오기 요청을 통해("/auth/authenticate-two-factor"`) 전달됩니다. 해당 사용자에게 사용자 인증 정보가 유효하면 사용자가 인증됩니다.
부수: 서버 코드 살펴보기
server.js
는 이미 일부 탐색 및 액세스를 처리하고 있으므로 인증된 사용자만 계정 페이지에 액세스할 수 있으며 필요한 리디렉션을 수행합니다.
이제 router.post("/initialize-authentication", ...
아래의 서버 코드를 살펴보겠습니다.
여기에는 두 가지 흥미로운 점이 있습니다.
- 이 단계에서 비밀번호와 사용자 인증 정보가 동시에 확인됩니다. 이는 보안 조치입니다. 2단계 인증을 설정한 사용자의 경우 비밀번호 절차가 올바른지에 따라 UI 흐름이 다르게 보이지 않기를 바랍니다. 이 단계에서 비밀번호와 사용자 인증 정보를 동시에 확인합니다.
- 비밀번호와 사용자 인증 정보가 모두 유효하면
completeAuthentication(req, res);
를 호출하여 인증을 완료합니다. 즉, 사용자가 아직 인증되지 않은 임시auth
세션에서 사용자가 인증된 기본 세션main
으로 세션을 전환한다는 의미입니다.
사용자 플로우에 2단계 인증 페이지를 포함합니다.
views
폴더에서 새 페이지 second-factor.html
를 확인합니다.
이 버튼에는 보안 키 사용이라고 표시된 버튼이 있지만 지금은 아무것도 하지 않습니다.
이 버튼을 클릭하면 authenticateTwoFactor()
을 호출합니다.
authenticateTwoFactor()
에 성공하면 사용자가 계정 페이지로 리디렉션됩니다.- 해결되지 않으면 사용자에게 오류가 발생했다고 알립니다. 실제 애플리케이션에서는 보다 유용한 오류 메시지를 구현합니다. 이 데모에서는 편의를 위해 창 알림만 사용합니다.
<main>
...
</main>
<script type="module">
import { authenticateTwoFactor, authStatuses } from "/auth.client.js";
const button = document.querySelector("#authenticateButton");
button.addEventListener("click", async e => {
try {
// Ask the user to authenticate with the second factor; this will trigger a browser prompt
const response = await authenticateTwoFactor();
const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
// The user is properly authenticated => Navigate to the Account page
location.href = "/account";
} else {
throw new Error("Two-factor authentication failed");
}
} catch (e) {
// Alert the user that something went wrong
alert(`Two-factor authentication failed. ${e}`);
}
});
</script>
</body>
</html>
2단계 인증 사용
이제 2단계 인증 단계를 추가할 수 있습니다.
이제 2단계 인증을 구성한 사용자를 위해 index.html
에 이 단계를 추가하기만 하면 됩니다.
index.html
의 location.href = "/account";
아래에 2단계 인증을 설정한 경우 사용자를 2단계 인증 페이지로 조건부로 탐색하는 코드를 추가합니다.
이 Codelab에서 사용자 인증 정보를 만들면 사용자가 자동으로 2단계 인증을 선택합니다.
또한 server.js
에서는 인증된 사용자만 account.html
에 액세스할 수 있도록 서버 측 세션 확인을 구현합니다.
const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
// The user is properly authenticated => navigate to account
location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
// Navigate to the two-factor-auth page because two-factor-auth is set up for this user
location.href = '/second-factor';
}
직접 시험해 보세요. 👩 💻
- 새 사용자 johndoe로 로그인합니다.
- 로그아웃합니다.
- johndoe로 계정에 로그인합니다. 비밀번호만 입력하면 됩니다.
- 사용자 인증 정보를 만듭니다. 이는 johndoe가 2단계 인증을 활성화했음을 의미합니다.
- 로그아웃합니다.
- 사용자 이름 johndoe와 비밀번호를 삽입합니다.
- 2단계 인증 페이지로 자동 이동하는 방법을 알아보세요.
/account
에서 계정 페이지에 액세스해 봅니다. 완전히 인증되지 않았으므로 색인 페이지로 리디렉션되는 방법을 확인할 수 있습니다. 두 번째 요소가 누락되어 있습니다.- 2단계 인증 페이지로 돌아가서 보안 키 사용을 클릭하여 2단계 인증을 합니다.
- 로그인되었으며 계정 페이지가 표시됩니다.
8. 사용자 인증 정보를 더 쉽게 사용
보안 키를 사용하여 2단계 인증의 기본 기능을 이용할 수 있습니다. 🚀
하지만... 눈치채셨나요?
현재 사용자 인증 정보 목록이 매우 편리하지 않습니다. 사용자 인증 정보 ID와 공개 키는 사용자 인증 정보를 관리하는 데 유용하지 않은 긴 문자열입니다. 긴 문자열과 숫자로는 인간이 바람직하지 않습니다 🚶
이를 개선하고, 사람이 읽을 수 있는 문자열로 사용자 인증 정보의 이름을 지정하고 이름을 변경하는 기능을 추가해 보겠습니다.
이름 사용자 인증 정보 살펴보기
이 기능을 구현하는 데 걸리는 시간을 크게 단축하기 위해 auth.client.js
에서 사용자 인증 정보 이름을 변경하는 함수를 시작 코드에 추가했습니다.
async function renameCredential(credId, newName) {
const params = new URLSearchParams({
credId,
name: newName
});
return _fetch(
`/auth/credential?${params}`,
"PUT"
);
}
이 메서드는 일반 데이터베이스 업데이트 호출입니다. 클라이언트는 사용자 인증 정보 ID와 해당 사용자 인증 정보의 새 이름을 사용하여 PUT
요청을 백엔드로 보냅니다.
커스텀 사용자 인증 정보 이름 구현
account.html
에서 빈 함수 rename
를 확인합니다.
다음 코드를 파일에 추가합니다.
// Rename a credential
async function rename(credentialId) {
// Let the user input a new name
const newName = window.prompt(`Name this credential:`);
// Rename only if the user didn't cancel AND didn't enter an empty name
if (newName && newName.trim()) {
try {
// Make the backend call to rename the credential (the name is sanitized) server-side
await renameCredential(credentialId, newName);
} catch (e) {
// Alert the user that something went wrong
if (Array.isArray(e)) {
alert(
// `msg` not `message`, this is the key's name as per the express validator API
`Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
);
} else {
alert(`Renaming failed. ${e}`);
}
}
// Refresh the credential list to display the new name
await updateCredentialList();
}
}
사용자 인증 정보가 성공적으로 만들어진 후에만 사용자 인증 정보의 이름을 지정하는 것이 좋습니다. 이름이 없는 사용자 인증 정보를 만든 다음, 만들고 나면 사용자 인증 정보의 이름을 변경합니다. 그러나 이 경우 백엔드 호출이 두 번 발생합니다.
등록 시 사용자가 사용자 인증 정보의 이름을 지정할 수 있도록 register()
에서 rename
함수를 사용합니다.
async function register() {
let user = {};
try {
const user = await registerCredential();
// Get the latest credential's ID (newly created credential)
const allUserCredentials = user.credentials;
const newCredential = allUserCredentials[allUserCredentials.length - 1];
// Rename it
await rename(newCredential.credId);
} catch (e) {
// ...
}
// Refresh the credential list to display the new credential
await updateCredentialList();
}
백엔드에서 사용자 입력이 검증 및 정리됩니다.
check("name")
.trim()
.escape()
사용자 인증 정보 이름 표시
templates.js
의 getCredentialHtml
로 이동합니다.
사용자 인증 정보 카드 상단에 사용자 인증 정보 이름을 표시하는 코드가 이미 있습니다.
// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
const { name, credId, publicKey } = credential;
return html`
<div class="credential-card">
<div class="credential-name">
${name
? html`
${name}
`
: html`
<span class="unnamed">(Unnamed)</span>
`}
</div>
// ...
</div>
`;
};
직접 시험해 보세요. 👩 💻
- 사용자 인증 정보를 만듭니다.
- 이름을 지정하라는 메시지가 표시됩니다.
- 새 이름을 입력하고 확인을 클릭합니다.
- 이제 사용자 인증 정보의 이름이 변경되었습니다.
- 이름 입력란을 비워두면 반복하고 원활하게 작동하는지 확인합니다.
사용자 인증 정보 이름 변경 사용 설정
사용자가 사용자 인증 정보의 이름을 변경해야 할 수도 있습니다. 예를 들어 두 번째 키를 추가하고 첫 번째 키의 이름을 변경하여 더 쉽게 구분할 수 있습니다.
account.html
에서 so-far-빈 함수 renameEl
를 찾아 다음 코드에 추가합니다.
// Rename a credential via HTML element
async function renameEl(el) {
// Define the ID of the credential to update
const credentialId = el.srcElement.dataset.credentialId;
// Rename the credential
await rename(credentialId);
// Refresh the credential list to display the new name
await updateCredentialList();
}
이제 templates.js
의 getCredentialHtml
에서 class="flex-end"
div 내에 다음 코드를 추가합니다. 이 코드는 사용자 인증 정보 카드 템플릿에 이름 바꾸기 버튼을 추가합니다. 버튼을 클릭하면 방금 만든 renameEl
함수가 호출됩니다.
const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
<div class="flex-end">
<button
data-credential-id="${credId}"
@click="${renameEl}"
class="secondary right"
>
Rename
</button>
</div>
// ...
`;
};
직접 시험해 보세요. 👩 💻
- 이름 바꾸기를 클릭합니다.
- 메시지가 표시되면 새 이름을 입력합니다.
- 확인을 클릭합니다.
- 사용자 인증 정보 이름을 변경하면 목록이 자동으로 업데이트됩니다.
- 페이지를 새로고침해도 새 이름이 계속 표시됩니다 (새 이름이 서버 측에서 유지됨).
사용자 인증 정보 생성 날짜 표시
생성 날짜가 navigator.credential.create()
을(를) 통해 생성된 사용자 인증 정보에 표시되지 않습니다.
그러나 이 정보는 사용자가 사용자 인증 정보를 구분하는 데 유용할 수 있으므로 시작 코드의 서버 측 라이브러리를 조정하고 새 사용자 인증 정보를 저장할 때 creationDate
와 동일한 creationDate
필드를 추가했습니다.
templates.js
의 class="creation-date"
div
내에 다음을 추가하여 생성 날짜 정보를 사용자에게 표시합니다.
<div class="creation-date">
<label>Created:</label>
<div class="info">
${new Date(creationDate).toLocaleDateString()}
${new Date(creationDate).toLocaleTimeString()}
</div>
</div>
9. 미래형 코드 만들기
지금까지는 사용자에게 간단한 로밍 인증자를 등록해 달라고 요청했고, 그 이후에는 로그인 시 2단계 인증으로 사용되었습니다.
한 가지 고급 접근 방식은 더 강력한 유형의 인증자인 사용자 확인 로밍 인증자 (UVRA)를 사용하는 것입니다. UVRA는 단일 인증 절차에서 두 가지 인증 요소 및 피싱 방지 기능을 제공할 수 있습니다.
두 가지 접근방식을 모두 지원하는 것이 이상적입니다. 이를 위해 다음 단계에 따라 사용자 환경을 맞춤설정해야 합니다.
- 사용자가 간단한 (사용자가 확인되지 않은) 로밍 인증자만 있는 경우 이를 사용하여 피싱 방지 계정 부트스트랩을 달성하지만 사용자 이름과 비밀번호도 입력해야 합니다. 이 Codelab은 이미 이 작업을 하고 있습니다.
- 다른 사용자가 고급 사용자 확인 로밍 인증자를 보유한 경우 계정 부트스트랩 시 비밀번호 단계를 건너뛰거나 사용자 이름 단계를 건너뛰어도 됩니다.
자세한 내용은 비밀번호가 없는 로그인 옵션을 사용하는 피싱 방지 계정 부트스트랩에서 확인하세요.
이 Codelab에서는 실제로 사용자 환경을 맞춤설정하지 않지만, 사용자 환경을 맞춤설정하는 데 필요한 데이터를 얻을 수 있도록 코드베이스를 설정합니다.
다음 두 가지가 필요합니다.
- 백엔드 설정에서
residentKey: preferred
를 설정하세요. 이 작업은 이미 완료되어 있습니다. - 검색 가능한 사용자 인증 정보 (거주 키라고도 함)가 생성되었는지 확인할 방법을 설정합니다.
검색 가능한 사용자 인증 정보가 생성되었는지 확인하려면 다음 안내를 따르세요.
- 사용자 인증 정보 생성 시
credProps
(credProps: true
) 값을 쿼리합니다. - 사용자 인증 정보 생성 시
transports
의 값을 쿼리합니다. 이를 통해 기본 플랫폼이 UVRA 기능(예: 휴대전화)을 지원하는지 확인할 수 있습니다. - 백엔드에
credProps
및transports
값을 저장합니다. 시작 코드에서 이 작업은 이미 완료되었습니다. 궁금하다면auth.js
을(를) 살펴보세요.
credProps
와 transports
의 값을 가져와 백엔드로 전송합니다. auth.client.js
에서 다음과 같이 registerCredential
를 수정합니다.
navigator.credentials.create
호출 시extensions
필드 추가- 스토리지의 백엔드에 사용자 인증 정보를 전송하기 전에
encodedCredential.transports
및encodedCredential.credProps
를 설정합니다.
registerCredential
는 다음과 같아야 합니다.
async function registerCredential() {
// Fetch the credential creation options from the backend
const credentialCreationOptionsFromServer = await _fetch(
'/auth/credential-options',
'POST'
);
// Decode the credential creation options
const credentialCreationOptions = decodeServerOptions(
credentialCreationOptionsFromServer
);
// Create a credential via the browser API; this will prompt the user
const credential = await navigator.credentials.create({
publicKey: {
...credentialCreationOptions,
extensions: {
credProps: true,
},
},
});
// Encode the newly created credential to send it to the backend
const encodedCredential = encodeCredential(credential);
// Set transports and credProps for more advanced user flows
encodedCredential.transports = credential.response.getTransports();
encodedCredential.credProps =
credential.getClientExtensionResults().credProps;
// Send the encoded credential to the backend for storage
return await _fetch('/auth/credential', 'POST', encodedCredential);
}
10. 교차 브라우저 지원 보장
Chromium 이외의 브라우저 지원
public/auth.client.js
의 registerCredential
함수에서 새로 만든 사용자 인증 정보에 대한 credential.response.getTransports()
를 호출하여 최종적으로 이 정보를 백엔드에 힌트로 저장합니다.
그러나 getTransports()
이 현재 모든 브라우저에서 지원되는 getClientExtensionResults
와 달리 구현되지 않습니다. getTransports()
을 호출하면 Firefox와 Safari에서 오류가 발생하므로 브라우저에서 사용자 인증 정보를 생성할 수 없습니다.
코드가 모든 주요 브라우저에서 실행되도록 하려면 조건에서 encodedCredential.transports
호출을 래핑합니다.
if (credential.response.getTransports) {
encodedCredential.transports = credential.response.getTransports();
}
서버에서 transports
은 transports || []
로 설정됩니다. Firefox 및 Safari에서 transports
목록이 undefined
가 아니고 빈 목록 []
가 있어 오류를 방지합니다.
WebAuthn을 지원하지 않는 브라우저를 사용하는 사용자에게 경고
WebAuthn이 모든 주요 브라우저에서 지원되지만 WebAuthn을 지원하지 않는 브라우저에 경고를 표시하는 것이 좋습니다.
index.html
에서 다음 div가 있는지 확인합니다.
<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>
index.html
인라인 스크립트에서 다음 코드를 추가하여 WebAuthn을 지원하지 않는 브라우저에 배너를 표시합니다.
// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
document.querySelector('#warningbanner').classList.remove('invisible');
}
실제 웹 애플리케이션에서는 보다 정교한 작업을 수행하고 이러한 브라우저에 적절한 대체 메커니즘을 제공하지만, WebAuthn 지원을 확인하는 방법을 살펴보겠습니다.
11. 잘하셨습니다.
✨
보안 키를 사용하여 2단계 인증을 구현했습니다.
이 Codelab에서는 기본사항을 다뤘습니다. 2단계 인증을 위한 WebAuthn을 자세히 살펴보려면 다음에 시도해 볼 수 있는 다음 아이디어를 참고해 보세요.
- 사용자 인증 정보 카드에 \'마지막으로 사용한 정보’ 정보를 추가합니다. 이는 사용자가 주어진 보안 키의 사용 여부를 결정할 때 특히 도움이 되는 정보입니다(특히 여러 개의 키를 등록한 경우).
- 더 강력한 오류 처리 및 보다 정확한 오류 메시지를 구현합니다.
auth.js
을 살펴보고 일부authSettings
가 변경되면 어떻게 되는지, 특히 사용자 인증을 지원하는 키를 사용할 때 어떤 일이 일어나는지 살펴보세요.