1. לפני שמתחילים
פתרונות אימות מסורתיים מעלים מספר אתגרים שקשורים לאבטחה ולנוחות השימוש.
סיסמאות נמצאות בשימוש נרחב, אבל…
- קל לשכוח
- המשתמשים צריכים לדעת איך ליצור סיסמאות חזקות.
- קל לנסות לבצע פישינג, לאסוף ולשחזר את המידע על ידי תוקפים.
ב-Android פועלים ליצירת Credential Manager API כדי לפשט את חוויית הכניסה ולטפל בסיכוני אבטחה. ה-API תומך במפתחות גישה, שהם תקן התעשייה מהדור הבא לאימות ללא סיסמה.
Credential Manager תומך במפתחות גישה ומשלב אותם עם שיטות אימות מסורתיות כמו סיסמאות, כניסה באמצעות חשבון Google וכו'.
המשתמשים יוכלו ליצור מפתחות גישה ולאחסן אותם במנהל הסיסמאות של Google, שיסנכרן את מפתחות הגישה האלה בין מכשירי Android שבהם המשתמש מחובר לחשבון. כדי שמשתמש יוכל להיכנס באמצעות מפתח גישה, צריך ליצור מפתח גישה, לשייך אותו לחשבון משתמש ולאחסן את המפתח הציבורי שלו בשרת.
בשיעור Codelab הזה תלמדו איך להירשם באמצעות מפתחות גישה וסיסמאות באמצעות Credential Manager API, ואיך להשתמש בהם למטרות אימות בעתיד. יש 2 תהליכי עבודה, כולל:
- הרשמה : באמצעות מפתחות גישה וסיסמה.
- כניסה לחשבון : באמצעות מפתחות גישה וסיסמאות שמורות.
דרישות מוקדמות
- הבנה בסיסית של אופן ההפעלה של אפליקציות ב-Android Studio.
- הבנה בסיסית של תהליך האימות באפליקציות ל-Android.
- הבנה בסיסית של מפתחות גישה.
מה תלמדו
- איך יוצרים מפתח גישה
- איך שומרים סיסמה במנהל סיסמאות.
- איך מאמתים משתמשים באמצעות מפתח גישה או סיסמה שמורה.
הדרישות
אחד משילובי המכשירים הבאים:
- מכשיר Android עם Android 9 ומעלה (למפתחות גישה) ו-Android 4.4 ומעלה(לאימות סיסמאות באמצעות Credential Manager API).
- מכשיר, רצוי עם חיישן ביומטרי.
- חשוב להגדיר שיטה לפתיחת הנעילה (ביומטרית או אחרת).
- גרסת הפלאגין של Kotlin : 1.8.10
2. להגדרה
אפליקציית הדוגמה הזו דורשת קישור של נכס דיגיטלי לאתר כדי שמנהל האישורים יוכל לאמת את הקישור ולהמשיך, ולכן מזהה ה-RP שמשמש בתשובות המדומות הוא משרת מדומיין של צד שלישי. אם רוצים לנסות תגובה מדומה משלכם, אפשר להוסיף את הדומיין של האפליקציה, ולא לשכוח להשלים את הקישור של הנכס הדיגיטלי כמו שמוסבר כאן.
כדי לאמת את הקישור של נכס דיגיטלי של שם החבילה ו-SHA בשרת הדמה, צריך להשתמש באותו debug.keystore שצוין בפרויקט כדי ליצור גרסאות ניפוי באגים וגרסאות הפצה. (הפעולה הזו כבר מתבצעת בשבילכם באפליקציית הדוגמה בקובץ build.gradle).
- משכפלים את המאגר הזה במחשב הנייד מהענף 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 Studio.
הצגת המצב ההתחלתי של האפליקציה
כדי לראות איך מצב ההתחלה של האפליקציה פועל, פועלים לפי השלבים הבאים:
- מפעילים את האפליקציה.
- יוצג מסך ראשי עם לחצני הרשמה וכניסה. הכפתורים האלה עדיין לא עושים כלום, אבל נפעיל את הפונקציונליות שלהם בקטעים הבאים.

3. הוספת אפשרות להירשם באמצעות מפתחות גישה
כשנרשמים לחשבון חדש באפליקציית Android שמשתמשת ב-Credential Manager API, המשתמשים יכולים ליצור מפתח גישה לחשבון שלהם. מפתח הגישה הזה יישמר בצורה מאובטחת אצל ספק אמצעי הזיהוי שהמשתמש בחר, וישמש לכניסות עתידיות לחשבון בלי שהמשתמש יצטרך להזין את הסיסמה בכל פעם.
עכשיו תיצרו מפתח גישה ותירשמו פרטי כניסה של משתמש באמצעות ביומטריה או שיטה לפתיחת נעילת המסך.
הרשמה באמצעות מפתח גישה
הקוד בתוך CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt מגדיר שדה טקסט בשם username ולחצן להרשמה באמצעות מפתח גישה.

הגדרת lambda של createCredential() לשימוש ב-View Models
אובייקטים של כלי לניהול פרטי הכניסה דורשים העברה של Activity שמשויך למסך. עם זאת, פעולות של Credential Manager מופעלות בדרך כלל ב-View Models, ולא מומלץ להפנות אל Activities בתוך View Models. לכן, אנחנו מגדירים את הפונקציות של Credential manager בקובץ נפרד CredentialManagerUtil.kt ומפנים אליהן במסכים המתאימים, שמעבירים אותן למודלים של התצוגה שלהם כקריאות חוזרות (callback) באמצעות פונקציות למדה.
מאתרים את התגובה TODO בפונקציה createCredential() ב-CredentialManagerUtil.kt ומפעילים את הפונקציה CredentialManager.create():
CredentialManagerUtil.kt
suspend fun createCredential(
activity: Activity,
request: CreateCredentialRequest
): CreateCredentialResponse {
TODO("Create a CredentialManager object and call createCredential() with a CreateCredentialRequest")
val credentialManager = CredentialManager.create(activity)
return credentialManager.createCredential(activity, request)
}
העברת האתגר ותגובת ה-JSON האחרת לקריאה ל-createPasskey()
לפני שיוצרים מפתח גישה, צריך לבקש מהשרת את המידע הדרוש להעברה אל Credential Manager API במהלך הקריאה ל-createCredential().
כבר יש לכם תגובה מדומה בנכסים של הפרויקט, שנקראת RegFromServer.txt, שמחזירה את הפרמטרים הנדרשים ב-codelab הזה.
- באפליקציה, עוברים אל
SignUpViewModel.kt, מוצאים את השיטהsignUpWithPasskeysשבה כותבים את הלוגיקה ליצירת מפתח גישה ולאפשר למשתמש להיכנס. אפשר למצוא את השיטה באותה כיתה. - מאתרים את בלוק התגובות
TODOעדcreate a CreatePublicKeyCredentialRequest()ומחליפים אותו בקוד הבא:
SignUpViewModel.kt
TODO("Create a CreatePublicKeyCredentialRequest() with necessary registration json from server")
val request = CreatePublicKeyCredentialRequest(
jsonProvider.fetchRegistrationJson()
.replace("<userId>", getEncodedUserId())
.replace("<userName>", _username.value)
.replace("<userDisplayName>", _username.value)
.replace("<challenge>", getEncodedChallenge())
)
ה-method jsonProvider.fetchRegistrationJsonFromServer() קוראת תגובת JSON של שרת מדומה PublicKeyCredentialCreationOptions מנכסים ומחזירה את ה-JSON של הרישום שיועבר במהלך יצירת מפתח הגישה. אנחנו מחליפים חלק מערכי הפלייסהולדר בערכים שהמשתמשים מזינים באפליקציה שלנו, ובחלק מהשדות המדומים:
- קובץ ה-JSON הזה לא מלא, ויש בו 4 שדות שצריך להחליף.
- מזהה המשתמש צריך להיות ייחודי כדי שמשתמש יוכל ליצור כמה מפתחות גישה (אם נדרש). מחליפים את
<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 בייט. זהו מאפיין חובה, אבל הוא לא בשימוש במהלך הרישום, אלא אם מבצעים אימות. | |
מזהה ייחודי של משתמש. הערך הזה לא יכול לכלול פרטים אישיים מזהים, למשל כתובות אימייל או שמות משתמש. ערך אקראי של 16 בייט שנוצר לכל חשבון יתאים. | |
בשדה הזה צריך להזין מזהה ייחודי של החשבון שהמשתמש יזהה, כמו כתובת האימייל או שם המשתמש שלו. הוא יוצג בבורר החשבונות. (אם משתמשים בשם משתמש, צריך להשתמש באותו ערך כמו באימות באמצעות סיסמה). | |
השדה הזה הוא אופציונלי, והוא מאפשר להזין שם ידידותי יותר למשתמש לחשבון. | |
הישות של הצד המסתמך תואמת לפרטי האפליקציה שלכם. המאפיינים שלו הם:
| |
רשימה של אלגוריתמים וסוגי מפתחות מותרים. הרשימה הזו חייבת להכיל לפחות רכיב אחד. | |
יכול להיות שהמשתמש שמנסה לרשום מכשיר רשם מכשירים אחרים. כדי להגביל את יצירתם של פרטי כניסה מרובים לאותו חשבון במאמת יחיד, אפשר להתעלם מהמכשירים האלה. אם מספקים את חבר | |
המדיניות מציינת אם המכשיר צריך להיות מצורף לפלטפורמה, או אם אין דרישה לעשות זאת. מגדירים את הערך הזה ל- | |
| מציינים את הערך |
יצירת פרטי כניסה
- אחרי שיוצרים
CreatePublicKeyCredentialRequest(), צריך להפעיל את הקריאהcreateCredential()עם הבקשה שנוצרה.
SignUpViewModel.kt
try {
TODO("Call createCredential() with createPublicKeyCredentialRequest")
createCredential(request)
TODO("Complete the registration process after sending public key credential to your server and let the user in")
} catch (e: CreateCredentialException) {
handlePasskeyFailure(e)
}
- אתם אחראים לטיפול בבעיות שקשורות למידת החשיפה של התצוגות המעובדות, ולטיפול בחריגים אם הבקשה נכשלת או לא מצליחה מסיבה כלשהי. הודעות השגיאה מתועדות ומוצגות באפליקציה בתיבת דו-שיח של שגיאה. אפשר לבדוק את יומני השגיאות המלאים דרך Android Studio או הפקודה
adb debug.
- לבסוף, צריך להשלים את תהליך ההרשמה. האפליקציה שולחת אישור מפתח ציבורי לשרת, והשרת רושם אותו למשתמש הנוכחי.
במקרה הזה השתמשנו בשרת מדומה, ולכן אנחנו מחזירים רק את הערך true כדי לציין שהשרת שמר את המפתח הציבורי הרשום לצורכי אימות עתידיים. מידע נוסף על רישום מפתחות גישה בצד השרת זמין להטמעה שלכם.
בתוך השיטה signUpWithPasskeys(), מוצאים את התגובה הרלוונטית ומחליפים אותה בקוד הבא:
SignUpViewModel.kt
try {
createCredential(request)
TODO("Complete the registration process after sending public key credential to your server and let the user in")
registerResponse()
DataProvider.setSignedInThroughPasskeys(true)
_navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = true))
} catch (e: CreateCredentialException) {
handlePasskeyFailure(e)
}
- הפונקציה
registerResponse()מחזירהtrue, שמציין שהמפתח הציבורי נשמר בשרת הדמה לשימוש עתידי. - מגדירים את הדגל
setSignedInThroughPasskeysלערךtrue. - אחרי שהמשתמש מתחבר, אתם מפנים אותו למסך הבית.
PublicKeyCredential אמיתי עשוי להכיל יותר שדות. דוגמה לשדות האלה:
{
"id": String,
"rawId": String,
"type": "public-key",
"response": {
"clientDataJSON": String,
"attestationObject": String,
}
}
בטבלה הבאה מפורטים כמה מהפרמטרים החשובים באובייקט PublicKeyCredential:
פרמטרים | תיאורים |
מזהה של מפתח הגישה שנוצר, בקידוד Base64URL. המזהה הזה עוזר לדפדפן לקבוע אם יש במכשיר מפתח גישה תואם בזמן האימות. הערך הזה צריך להיות מאוחסן במסד הנתונים בקצה העורפי. | |
גרסת האובייקט | |
אובייקט | |
אובייקט אימות בקידוד |
מריצים את האפליקציה, ואז לוחצים על הלחצן הרשמה באמצעות מפתחות גישה ויוצרים מפתח גישה.
4. שמירת סיסמה בספק אישורים
באפליקציה הזו, במסך ההרשמה, כבר הטמעתם הרשמה באמצעות שם משתמש וסיסמה למטרות הדגמה.
כדי לשמור את פרטי הכניסה של סיסמת המשתמש אצל ספק הסיסמאות שלו, צריך להטמיע CreatePasswordRequest כדי להעביר אל createCredential() את הסיסמה לשמירה.
- מחפשים את השיטה
signUpWithPassword()ומחליפים את TODO בקריאה ל-createPassword:
SignUpViewModel.kt
TODO("CreatePasswordRequest with entered username and password")
val passwordRequest = CreatePasswordRequest(_username.value, _password.value)
- לאחר מכן, יוצרים פרטי כניסה באמצעות בקשה ליצירת סיסמה ושומרים את פרטי הכניסה של סיסמת המשתמש אצל ספק הסיסמאות שלו. לאחר מכן, מתחברים לחשבון של המשתמש. אנחנו מאתרים חריגים שמתרחשים בתהליך הזה באופן כללי יותר. מחליפים את TODO בקוד הבא:
SignUpViewModel.kt
TODO("Create credential with created password request and log the user in")
try {
createCredential(passwordRequest)
simulateServerDelayAndLogIn()
} catch (e: Exception) {
val errorMessage = "Exception Message : " + e.message
Log.e("Auth", errorMessage)
_passwordCreationError.value = errorMessage
_isLoading.value = false
}
עכשיו שמרתם בהצלחה את פרטי הכניסה של הסיסמה אצל ספק הסיסמאות של המשתמש, כדי לבצע אימות באמצעות סיסמה בהקשה אחת בלבד.
5. הוספת אפשרות לאימות באמצעות מפתח גישה או סיסמה
עכשיו אפשר להשתמש בו כדי לאמת את הזהות שלכם באפליקציה בצורה בטוחה.

הגדרת lambda של getCredential() לשימוש ב-View Models
כמו קודם, נקרא ל-getCredential() של Credential manager בקובץ נפרד CredentialManagerUtil.kt לצורך הפניה במסכים המתאימים והעברה למודלים של התצוגה שלהם כקריאות חוזרות באמצעות פונקציות lambda.
מאתרים את התגובה TODO בפונקציה getCredential() ב-CredentialManagerUtil.kt ומפעילים את הפונקציה CredentialManager.get():
suspend fun getCredential(
activity: Activity,
request: GetCredentialRequest
): GetCredentialResponse {
TODO("Create a CredentialManager object and call getCredential() with a GetCredentialRequest")
val credentialManager = CredentialManager.create(activity)
return credentialManager.getCredential(activity, request)
}
קבלת האתגר ואפשרויות אחרות להעברה לקריאה ל-getPasskey()
לפני שמבקשים מהמשתמש לבצע אימות, צריך לבקש פרמטרים להעברה ב-JSON של WebAuthn מהשרת, כולל אתגר.
כבר יש לכם תגובה מדומה בנכסים שלכם (AuthFromServer.txt) שמחזירה פרמטרים כאלה ב-codelab הזה.
- באפליקציה, עוברים אל SignInViewModel.kt, מוצאים את השיטה
signInWithSavedCredentialsשבה כותבים את הלוגיקה לאימות באמצעות מפתח גישה או סיסמה שמורים, ומאפשרים למשתמש להיכנס: - יוצרים GetPublicKeyCredentialOption() עם הפרמטרים הנדרשים כדי לקבל אישורים מספק האישורים.
SignInViewModel.kt
TODO("Create a GetPublicKeyCredentialOption() with necessary authentication json from server")
val getPublicKeyCredentialOption =
GetPublicKeyCredentialOption(jsonProvider.fetchAuthJson(), null)
השיטה fetchAuthJsonFromServer() קוראת את תגובת ה-JSON של האימות מנכסים ומחזירה JSON של אימות כדי לאחזר את כל מפתחות הגישה שמשויכים לחשבון המשתמש הזה.
הפרמטר השני של GetPublicKeyCredentialOption() הוא clientDataHash – גיבוב שמשמש לאימות הזהות של הצד הנסמך. מגדירים את האפשרות הזו רק אם הגדרתם את GetCredentialRequest.origin. באפליקציה לדוגמה, הערך הזה מוגדר ל-null.
הערה : השרת של ה-codelab הזה מתוכנן להחזיר JSON שדומה ככל האפשר למילון PublicKeyCredentialRequestOptions שמועבר לקריאה getCredential() של ה-API. בקטע הקוד הבא יש כמה דוגמאות לאפשרויות שאפשר לקבל בתגובה אמיתית:
{
"challenge": String,
"rpId": String,
"userVerification": "",
"timeout": 1800000
}
בטבלה הבאה מפורטים כמה מהפרמטרים החשובים באובייקט PublicKeyCredentialRequestOptions:
פרמטרים | תיאורים |
אתגר שנוצר על ידי השרת באובייקט | |
מזהה RP הוא דומיין. באתר אפשר לציין את הדומיין או סיומת שאפשר לרשום. הערך הזה צריך להיות זהה לערך של הפרמטר |
- לאחר מכן צריך ליצור אובייקט
PasswordOption()כדי לאחזר את כל הסיסמאות השמורות שספק הסיסמאות שמר באמצעות Credential Manager API עבור חשבון המשתמש הזה. בתוךgetSavedCredentials()method, מחפשים את TODO ומחליפים אותו בקוד הבא:
SigninViewModel.kt
TODO("Create a PasswordOption to retrieve all the associated user's password")
val getPasswordOption = GetPasswordOption()
משלבים את הנתונים האלה לGetCredentialRequest.
SigninViewModel.kt
TODO("Combine requests into a GetCredentialRequest")
val request = GetCredentialRequest(
listOf(
getPublicKeyCredentialOption,
getPasswordOption
)
)
קבלת פרטי כניסה
לאחר מכן צריך להפעיל את בקשת getCredential() עם כל האפשרויות שלמעלה כדי לאחזר את פרטי הכניסה המשויכים:
SignInViewModel.kt
try {
TODO("Call getCredential() with required credential options")
val result = getCredential(request)
val data = when (result.credential) {
is PublicKeyCredential -> {
val cred = result.credential as PublicKeyCredential
DataProvider.setSignedInThroughPasskeys(true)
"Passkey: ${cred.authenticationResponseJson}"
}
is PasswordCredential -> {
val cred = result.credential as PasswordCredential
DataProvider.setSignedInThroughPasskeys(false)
"Got Password - User:${cred.id} Password: ${cred.password}"
}
is CustomCredential -> {
//If you are also using any external sign-in libraries, parse them here with the utility functions provided.
null
}
else -> null
}
TODO("Complete the authentication process after validating the public key credential to your server and let the user in.")
} catch (e: Exception) {
Log.e("Auth", "getCredential failed with exception: " + e.message.toString())
_signInError.value =
"An error occurred while authenticating: " + e.message.toString()
} finally {
_isLoading.value = false
}
- מעבירים את המידע הנדרש אל
getCredential(). הפונקציה הזו מקבלת את רשימת האפשרויות של פרטי הכניסה ואת ההקשר של הפעילות כדי להציג את האפשרויות בגיליון התחתון בהקשר הזה. - אחרי שהבקשה תעובד בהצלחה, יופיע בחלק התחתון של המסך גיליון עם רשימה של כל פרטי הכניסה שנוצרו לחשבון המשויך.
- עכשיו המשתמשים יכולים לאמת את הזהות שלהם באמצעות נתונים ביומטריים, שיטה לפתיחת הנעילה וכו', כדי לאמת את פרטי הכניסה שנבחרו.
- אם פרטי הכניסה שנבחרו הם
PublicKeyCredential, מגדירים את הדגלsetSignedInThroughPasskeysכ-true. אחרת, מגדירים את הערךfalse.
קטע הקוד הבא כולל אובייקט לדוגמה PublicKeyCredential:
{
"id": String
"rawId": String
"type": "public-key",
"response": {
"clientDataJSON": String
"authenticatorData": String
"signature": String
"userHandle": String
}
}
הטבלה הבאה לא כוללת את כל הפרמטרים, אבל היא כוללת את הפרמטרים החשובים באובייקט PublicKeyCredential:
פרמטרים | תיאורים |
המזהה בקידוד Base64URL של פרטי הכניסה של מפתח הגישה המאומת. | |
גרסת האובייקט | |
אובייקט | |
אובייקט | |
אובייקט | |
אובייקט |
- לבסוף, צריך להשלים את תהליך האימות. בדרך כלל, אחרי שהמשתמש משלים את האימות באמצעות מפתח גישה, האפליקציה שולחת לשרת פרטי כניסה של מפתח ציבורי שמכילים טענת אימות. השרת מאמת את הטענה ומאמת את המשתמש.
כאן השתמשנו בשרת מדומה, ולכן אנחנו מחזירים רק true כדי לציין שהשרת אימת את הטענה. אפשר לקרוא מידע נוסף על אימות מפתחות גישה בצד השרת לצורך הטמעה משלכם.
בתוך השיטה signInWithSavedCredentials(), מוצאים את ההערה הרלוונטית ומחליפים אותה בקוד הבא:
SignInViewModel.kt
TODO("Complete the authentication process after validating the public key credential to your server and let the user in.")
if (data != null) {
sendSignInResponseToServer()
_navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = DataProvider.isSignedInThroughPasskeys()))
}
-
sendSigninResponseToServer()מחזירה true, שמציין שהשרת (המדמה) אימת את המפתח הציבורי לשימוש עתידי. - אחרי שהמשתמש מתחבר, אתם מפנים אותו למסך הבית.
מריצים את האפליקציה ועוברים אל כניסה > כניסה באמצעות מפתחות גישה או סיסמה שמורה,ומנסים להיכנס באמצעות פרטי כניסה שמורים.
אני רוצה לנסות
הטמעתם את היצירה של מפתחות גישה, את שמירת הסיסמה ב-Credential Manager ואת האימות באמצעות מפתחות גישה או סיסמה שמורה באמצעות Credential Manager API באפליקציית Android.
6. מעולה!
סיימתם את ה-Codelab הזה! אם רוצים לבדוק את הפתרון הסופי, שזמין בכתובת https://github.com/android/identity-samples/tree/main/CredentialManager
אם יש לכם שאלות, אתם יכולים לשאול אותן ב-StackOverflow עם התג passkey.