1. 시작하기 전에
기존 인증 솔루션은 여러 보안 및 사용성 문제가 있습니다.
비밀번호는 널리 사용되지만 다음과 같은 문제가 있습니다.
- 쉽게 잊어버림
- 안전한 비밀번호를 만들려면 사용자에게 지식이 요구됨
- 공격자가 피싱하고 수집하고 재생하기 쉬움
Android에서는 비밀번호 없는 인증의 차세대 업계 표준인 패스키를 지원하여 로그인 환경을 간소화하고 보안 위험을 해결하는 Credential Manager API를 만들기 위해 노력했습니다.
인증 관리자는 패스키 지원을 통합하고 이를 비밀번호, Google 계정으로 로그인 등 기존 인증 방법과 결합합니다.
사용자는 패스키를 만들어 Google 비밀번호 관리자에 저장할 수 있으며 Google 비밀번호 관리자는 이러한 패스키를 사용자가 로그인하는 다양한 Android 기기에서 동기화합니다. 패스키를 생성하여 사용자 계정과 연결해야 하며, 공개 키를 서버에 저장해야 사용자가 로그인할 수 있습니다.
이 Codelab에서는 Credential Manager API를 사용하여 패스키와 비밀번호로 가입하고 이를 향후 인증용으로 사용하는 방법을 알아봅니다. 다음 두 가지 흐름이 있습니다.
- 가입: 패스키와 비밀번호 사용
- 로그인: 패스키와 저장된 비밀번호 사용
기본 요건
- Android 스튜디오에서 앱을 실행하는 방법에 관한 기본 이해
- Android 앱의 인증 흐름에 관한 기본 이해
- 패스키에 관한 기본 이해
학습할 내용
- 패스키를 생성하는 방법
- 비밀번호 관리자에 비밀번호를 저장하는 방법
- 패스키나 저장된 비밀번호로 사용자를 인증하는 방법
필요한 항목
다음 기기 조합 중 하나:
- Android 9 이상(패스키용) 및 Android 4.4 이상(Credential Manager API를 통한 비밀번호 인증용)을 실행하는 Android 기기
- 생체 인식 센서가 적용된 기기면 더 좋음
- 생체 인식(또는 화면 잠금)을 등록해야 함
- Kotlin 플러그인 버전: 1.8.10
2. 설정
- credman_codelab 브랜치(https://github.com/android/identity-samples/tree/credman_codelab)에서 노트북에 이 저장소를 클론합니다.
git clone -b credman_codelab https://github.com/android/identity-samples.git
- CredentialManager 모듈로 이동하여 Android 스튜디오에서 프로젝트를 엽니다.
앱의 초기 상태 확인
앱의 초기 상태가 어떻게 작동하는지 확인하려면 다음 단계를 따르세요.
- 앱을 실행합니다.
- 가입 및 로그인 버튼이 있는 기본 화면이 표시됩니다. 이 버튼은 아직 아무것도 하지 않지만 다음 섹션에서 기능을 사용 설정할 예정입니다.
3. 패스키를 사용한 가입 기능 추가
Credential Manager API를 사용하는 Android 앱에서 새 계정에 가입할 때 사용자는 계정의 패스키를 만들 수 있습니다. 이 패스키는 사용자가 선택한 사용자 인증 정보 제공업체에 안전하게 저장되어 향후 로그인에 사용되므로 사용자가 매번 비밀번호를 입력하지 않아도 됩니다.
이제 패스키를 만들고 생체 인식/화면 잠금을 사용하여 사용자 인증 정보를 등록합니다.
패스키로 가입
Credential Manager/app/main/java/SignUpFragment.kt 내의 코드는 'username' 텍스트 필드와 패스키로 가입하는 버튼을 정의합니다.
챌린지와 기타 json 응답을 createPasskey() 호출에 전달
패스키를 만들기 전에 createCredential 호출 중 Credential Manager API에 전달할 필요한 정보를 서버에 요청해야 합니다.
이 Codelab에서 필요한 매개변수를 반환하는 모의 응답(RegFromServer.txt)이 프로젝트의 애셋에 이미 있습니다.
- 앱에서 SignUpFragment.kt로 이동하여 signUpWithPasskeys 메서드를 찾습니다. 여기서 패스키 생성 및 사용자 가입을 위한 로직을 작성합니다. 동일한 클래스에서 이 메서드를 찾을 수 있습니다.
createPasskey()
를 호출하는 주석이 있는 else 블록을 확인하고 다음 코드로 바꿉니다.
SignUpFragment.kt
//TODO : Call createPasskey() to signup with passkey
val data = createPasskey()
이 메서드는 유효한 사용자 이름이 화면에 입력되면 호출됩니다.
createPasskey()
메서드 내에서 필요한 매개변수가 반환된CreatePublicKeyCredentialRequest()
를 만들어야 합니다.
SignUpFragment.kt
//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server
val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())
fetchRegistrationJsonFromServer()
메서드는 애셋에서 에뮬레이션된 서버 PublicKeyCredentialCreationOptions
JSON 응답을 읽고 패스키를 생성하는 동안 전달할 등록 JSON을 반환합니다.
fetchRegistrationJsonFromServer()
메서드를 찾고 TODO를 다음 코드로 대체하여 JSON을 반환하고 빈 문자열 return 문도 삭제합니다.
SignUpFragment.kt
//TODO fetch registration mock response
val response = requireContext().readFromAsset("RegFromServer")
//Update userId,challenge, name and Display name in the mock
return response.replace("<userId>", getEncodedUserId())
.replace("<userName>", binding.username.text.toString())
.replace("<userDisplayName>", binding.username.text.toString())
.replace("<challenge>", getEncodedChallenge())
- 이 JSON은 불완전하며 교체해야 할 필드가 4개 있습니다.
- UserId는 고유해야 하므로 사용자가 여러 패스키를 만들 수 있습니다(필요한 경우).
<userId>
를 생성된userId
값으로 바꿉니다. <challenge>
도 고유해야 하므로 임의의 고유한 챌린지를 생성하게 됩니다. 이 메서드는 이미 코드에 있습니다.
실제 서버 PublicKeyCredentialCreationOptions
응답은 더 많은 옵션을 반환할 수 있습니다. 이러한 필드의 예는 다음과 같습니다.
{
"challenge": String,
"rp": {
"name": String,
"id": String
},
"user": {
"id": String,
"name": String,
"displayName": String
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
],
"timeout": 1800000,
"attestation": "none",
"excludeCredentials": [],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"requireResidentKey": true,
"residentKey": "required",
"userVerification": "required"
}
}
다음 표에서는 PublicKeyCredentialCreationOptions
객체의 몇 가지 중요한 매개변수를 설명합니다.
매개변수 | 설명 |
엔트로피를 충분히 포함하여 추측을 불가능하게 하는 서버 생성 임의 문자열입니다. 길이는 16바이트 이상이어야 합니다. 이는 필수이지만 증명을 실행하지 않는 한 등록 중에 사용되지 않습니다. | |
사용자의 고유 ID입니다. 이 값에는 이메일 주소나 사용자 이름 등 개인 식별 정보가 포함되면 안 됩니다. 계정당 생성된 임의의 16바이트 값이 좋습니다. | |
이 필드에는 이메일 주소나 사용자 이름 등 사용자가 인식할 계정의 고유 식별자가 있어야 합니다. 이것은 계정 선택기에 표시됩니다. 사용자 이름을 사용하는 경우 비밀번호 인증에서와 동일한 값을 사용하세요. | |
이 필드는 선택사항이며 좀 더 사용자 친화적인 계정 이름입니다. | |
신뢰 당사자 엔티티는 애플리케이션 세부정보에 해당합니다. 다음과 같은 속성이 있습니다.
| |
허용된 알고리즘 및 키 유형 목록 목록에는 하나 이상의 요소가 포함되어야 합니다. | |
기기를 등록하려는 사용자가 다른 기기를 등록했을 수 있습니다. 단일 인증자에서 동일한 계정의 사용자 인증 정보를 여러 개 생성하는 것을 제한하려면 이러한 기기를 무시하면 됩니다. 제공되는 경우 | |
기기를 플랫폼에 연결해야 하는지 또는 이에 관한 요구사항이 없는지 나타냅니다. 이 값을 | |
| 패스키를 만들려면 값 |
사용자 인증 정보 만들기
CreatePublicKeyCredentialRequest()
를 만든 후에는 생성된 요청과 함께createCredential()
를 호출해야 합니다.
SignUpFragment.kt
//TODO call createCredential() with createPublicKeyCredentialRequest
try {
response = credentialManager.createCredential(
requireActivity(),
request
) as CreatePublicKeyCredentialResponse
} catch (e: CreateCredentialException) {
configureProgress(View.INVISIBLE)
handlePasskeyFailure(e)
}
- 필수 정보를
createCredential()
에 전달합니다. - 요청이 성공하면 패스키를 만들라는 메시지가 화면 하단 시트에 표시됩니다.
- 이제 사용자가 생체 인식이나 화면 잠금 등을 통해 신원을 확인할 수 있습니다.
- 렌더링된 뷰 공개 상태를 처리하고 어떤 이유로 인해 요청이 실패하는 경우 예외를 처리합니다. 여기서 오류 메시지가 기록되고 앱의 오류 대화상자에 표시됩니다. Android 스튜디오나
adb debug
명령어를 통해 전체 오류 로그를 확인할 수 있습니다.
- 마지막으로 등록 절차를 완료해야 합니다. 앱은 공개 키 사용자 인증 정보를 서버로 전송하고 서버는 이를 현재 사용자에게 등록합니다.
여기서는 모의 서버를 사용했으므로 서버가 등록된 공개 키를 향후 인증 및 검증용으로 저장했음을 나타내는 true를 반환합니다. 자체 구현을 위해 서버 측 패스키 등록에 대해 자세히 알아보세요.
signUpWithPasskeys()
메서드 내에서 관련 주석을 찾아 다음 코드로 바꿉니다.
SignUpFragment.kt
//TODO : complete the registration process after sending public key credential to your server and let the user in
data?.let {
registerResponse()
DataProvider.setSignedInThroughPasskeys(true)
listener.showHome()
}
registerResponse()
는 모의 서버가 향후 사용을 위해 공개 키를 저장했음을 나타내는true
를 반환합니다.setSignedInThroughPasskeys
플래그를true
로 설정합니다.- 로그인한 후에는 사용자를 홈 화면으로 리디렉션합니다.
실제 PublicKeyCredential
에는 더 많은 필드가 포함될 수 있습니다. 이러한 필드의 예는 다음과 같습니다.
{
"id": String,
"rawId": String,
"type": "public-key",
"response": {
"clientDataJSON": String,
"attestationObject": String,
}
}
다음 표에서는 PublicKeyCredential
객체의 몇 가지 중요한 매개변수를 설명합니다.
매개변수 | 설명 |
생성된 패스키의 Base64URL로 인코딩된 ID입니다. 이 ID는 인증 시 브라우저에서 기기에 일치하는 패스키가 있는지 확인하는 데 도움이 됩니다. 이 값은 백엔드의 데이터베이스에 저장해야 합니다. | |
사용자 인증 정보 ID의 | |
| |
|
앱을 실행하면 패스키로 가입 버튼을 클릭하여 패스키를 만들 수 있습니다.
4. 사용자 인증 정보 제공업체에 비밀번호 저장
이 앱의 가입 화면에는 이미 사용자 이름과 비밀번호를 사용한 가입이 데모용으로 구현되어 있습니다.
비밀번호 제공업체와 함께 사용자 비밀번호 사용자 인증 정보를 저장하려면 CreatePasswordRequest
를 구현하여 createCredential()
에 전달하여 비밀번호를 저장합니다.
signUpWithPassword()
메서드를 찾아 TODO를createPassword
호출로 바꿉니다.
SignUpFragment.kt
//TODO : Save the user credential password with their password provider
createPassword()
createPassword()
메서드 내에서 이와 같은 비밀번호 요청을 만들고 TODO를 다음 코드로 바꿔야 합니다.
SignUpFragment.kt
//TODO : CreatePasswordRequest with entered username and password
val request = CreatePasswordRequest(
binding.username.text.toString(),
binding.password.text.toString()
)
- 그런 다음
createPassword()
메서드 내에서 비밀번호 생성 요청으로 사용자 인증 정보를 만들고 사용자 비밀번호 사용자 인증 정보를 비밀번호 제공업체와 함께 저장합니다. TODO를 다음 코드로 바꿉니다.
SignUpFragment.kt
//TODO : Create credential with created password request
try {
credentialManager.createCredential(requireActivity(), request) as CreatePasswordResponse
} catch (e: Exception) {
Log.e("Auth", " Exception Message : " + e.message)
}
- 이제 탭 한 번으로 비밀번호로 인증하도록 사용자의 비밀번호 제공업체와 함께 비밀번호 사용자 인증 정보를 저장했습니다.
5. 패스키나 비밀번호를 사용한 인증 기능 추가
이제 앱에 안전하게 인증하기 위한 방법으로 이를 사용할 수 있습니다.
챌린지와 기타 옵션을 획득하여 getPasskey() 호출에 전달
사용자에게 인증을 요청하기 전에 챌린지를 포함하여 서버에서 WebAuthn JSON을 전달할 매개변수를 요청해야 합니다.
이 Codelab에서는 이러한 매개변수를 반환하는 모의 응답이 애셋 (AuthFromServer.txt)에 이미 있습니다.
- 앱에서 SignInFragment.kt로 이동하여
signInWithSavedCredentials
메서드를 찾아 저장된 패스키나 비밀번호를 통해 인증하는 로직을 작성해 사용자의 로그인을 허용합니다. createPasskey()
를 호출하는 주석이 있는 else 블록을 확인하고 다음 코드로 바꿉니다.
SignInFragment.kt
//TODO : Call getSavedCredentials() method to signin using passkey/password
val data = getSavedCredentials()
- getSavedCredentials() 메서드 내에서 사용자 인증 정보 제공업체에서 사용자 인증 정보를 가져오는 데 필요한 매개변수를 사용하여
GetPublicKeyCredentialOption()
를 만들어야 합니다.
SigninFragment.kt
//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server
val getPublicKeyCredentialOption =
GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)
fetchAuthJsonFromServer()
메서드는 애셋에서 인증 JSON 응답을 읽고 인증 JSON을 반환하여 이 사용자 계정과 연결된 모든 패스키를 가져옵니다.
GetPublicKeyCredentialOption()의 두 번째 매개변수인 clientDataHash
는 신뢰 당사자 ID를 확인하는 데 사용되는 해시입니다. GetCredentialRequest.origin
를 설정한 경우에만 설정합니다. 샘플 앱에서는 null
로 설정됩니다.
- fetchAuthJsonFromServer() 메서드를 찾고 TODO를 다음 코드로 대체하여 json을 반환하고 빈 문자열 return 문도 삭제합니다.
SignInFragment.kt
//TODO fetch authentication mock json
return requireContext().readFromAsset("AuthFromServer")
참고: 이 Codelab의 서버는 API의 getCredential() 호출에 전달된 PublicKeyCredentialRequestOptions
사전과 최대한 유사한 JSON을 반환하도록 설계되었습니다. 다음 코드 스니펫에는 실제 응답에서 수신할 수 있는 옵션 예가 포함되어 있습니다.
{
"challenge": String,
"rpId": String,
"userVerification": "",
"timeout": 1800000
}
다음 표에서는 PublicKeyCredentialRequestOptions
객체의 몇 가지 중요한 매개변수를 설명합니다.
매개변수 | 설명 |
| |
RP ID는 도메인입니다. 웹사이트에서 도메인 또는 등록 가능한 접미사를 지정할 수 있습니다. 이 값은 패스키 생성 시 사용된 |
- 이제
PasswordOption()
객체를 만들어 이 사용자 계정의 Credential Manager API를 통해 비밀번호 제공업체에 저장된 모든 비밀번호를 가져와야 합니다.getSavedCredentials()
메서드 내에서 TODO를 찾아 다음과 같이 바꿉니다.
SigninFragment.kt
//TODO create a PasswordOption to retrieve all the associated user's password
val getPasswordOption = GetPasswordOption()
사용자 인증 정보 가져오기
- 이제 위의 모든 옵션과 함께
getCredential()
요청을 호출하여 연결된 사용자 인증 정보를 가져와야 합니다.
SignInFragment.kt
//TODO call getCredential() with required credential options
val result = try {
credentialManager.getCredential(
requireActivity(),
GetCredentialRequest(
listOf(
getPublicKeyCredentialOption,
getPasswordOption
)
)
)
} catch (e: Exception) {
configureViews(View.INVISIBLE, true)
Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
activity?.showErrorAlert(
"An error occurred while authenticating through saved credentials. Check logs for additional details"
)
return null
}
if (result.credential is PublicKeyCredential) {
val cred = result.credential as PublicKeyCredential
DataProvider.setSignedInThroughPasskeys(true)
return "Passkey: ${cred.authenticationResponseJson}"
}
if (result.credential is PasswordCredential) {
val cred = result.credential as PasswordCredential
DataProvider.setSignedInThroughPasskeys(false)
return "Got Password - User:${cred.id} Password: ${cred.password}"
}
if (result.credential is CustomCredential) {
//If you are also using any external sign-in libraries, parse them here with the utility functions provided.
}
- 필수 정보를
getCredential()
에 전달합니다. 그러면 사용자 인증 정보 옵션 목록과 활동 컨텍스트를 사용하여 컨텍스트 하단 시트의 옵션을 렌더링합니다. - 요청이 성공하면 연결된 계정에 관해 생성된 모든 사용자 인증 정보가 화면의 하단 시트에 나열됩니다.
- 이제 사용자가 생체 인식이나 화면 잠금 등을 통해 신원을 확인하여, 선택한 사용자 인증 정보를 인증할 수 있습니다.
- 선택한 사용자 인증 정보가
PublicKeyCredential
인 경우setSignedInThroughPasskeys
플래그를true
로 설정합니다. 그렇지 않으면false
로 설정합니다.
다음 코드 스니펫에는 PublicKeyCredential
객체의 예시가 포함되어 있습니다.
{
"id": String
"rawId": String
"type": "public-key",
"response": {
"clientDataJSON": String
"authenticatorData": String
"signature": String
"userHandle": String
}
}
다음 표에 모든 내용이 포함되지는 않지만 PublicKeyCredential
객체에 있는 중요한 매개변수가 포함되어 있습니다.
매개변수 | 설명 |
인증된 패스키 사용자 인증 정보의 Base64URL로 인코딩된 ID입니다. | |
사용자 인증 정보 ID의 | |
클라이언트 데이터의 | |
인증자 데이터의 | |
서명의 | |
생성 시 설정된 사용자 ID를 포함하는 |
- 마지막으로 인증 절차를 완료해야 합니다. 일반적으로 사용자가 패스키 인증을 완료하면 앱은 인증 어설션이 포함된 공개 키 사용자 인증 정보를 서버로 전송하여 어설션을 확인하고 사용자를 인증합니다.
여기서는 모의 서버를 사용했으므로 서버에서 어설션을 확인했음을 나타내는 true
를 반환합니다. 자체 구현을 위해 서버 측 패스키 인증에 대해 자세히 알아보세요.
signInWithSavedCredentials()
메서드 내에서 관련 주석을 찾아 다음 코드로 바꿉니다.
SignInFragment.kt
//TODO : complete the authentication process after validating the public key credential to your server and let the user in.
data?.let {
sendSignInResponseToServer()
listener.showHome()
}
sendSigninResponseToServer()
는 (모의) 서버에서 향후 사용을 위해 공개 키를 검증했음을 나타내는 true를 반환합니다.- 로그인한 후에는 사용자를 홈 화면으로 리디렉션합니다.
앱을 실행하고 로그인 > 패스키/저장된 비밀번호로 로그인으로 이동하여 저장된 사용자 인증 정보를 사용하여 로그인해 보세요.
직접 해 보기
패스키를 생성하고 인증 관리자에 비밀번호를 저장하고 Android 앱에서 Credential Manager API를 사용하여 패스키나 저장된 비밀번호를 통해 인증하는 작업을 구현했습니다.
6. 축하합니다
이 Codelab을 완료했습니다. 최종 해결 방법은 https://github.com/android/identity-samples/tree/main/CredentialManager에서 확인할 수 있습니다.
궁금한 점이 있다면 StackOverflow에서 passkey
태그를 사용하여 질문해 주세요.