1. לפני שמתחילים
פתרונות האימות המסורתיים מציבים כמה אתגרים מבחינת אבטחה ונוחות השימוש.
הסיסמאות נמצאות בשימוש נרחב, אבל...
- קל לשכוח
- המשתמשים זקוקים לידע כדי ליצור סיסמאות חזקות.
- תוקפים יכולים לבצע פישינג, לקצור ולשחק שוב בקלות.
ב-Android השקיעו מאמצים כדי ליצור את Credential Manager API כדי לפשט את חוויית הכניסה ולטפל בסיכוני אבטחה באמצעות מפתחות גישה, הדור הבא בתחום לאימות ללא סיסמאות.
הכלי לניהול פרטי הכניסה משלב תמיכה במפתחות גישה ומשלב אותה עם שיטות אימות מסורתיות כמו סיסמאות, כניסה באמצעות חשבון 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. להגדרה
- אפשר לשכפל את המאגר הזה במחשב הנייד מהסתעפות credman_codelab : https://github.com/android/identity-samples/tree/credman_codelab
- עוברים למודול CredentialManager ופותחים את הפרויקט ב-Android Studio.
עכשיו אפשר לראות את המצב הראשוני של האפליקציה
כדי לראות איך פועל המצב הראשוני של האפליקציה, מבצעים את השלבים הבאים:
- מפעילים את האפליקציה.
- יוצג מסך ראשי עם לחצן הרשמה וכניסה.
- אפשר ללחוץ על 'הרשמה' כדי להירשם באמצעות מפתח גישה או סיסמה.
- אפשר ללחוץ על כניסה כדי להיכנס באמצעות מפתח גישה סיסמה שמורה.
כדי להבין מהם מפתחות גישה ואיך הם פועלים, תוכלו לעיין במאמר איך פועלים מפתחות גישה? .
3. הוספת היכולת להירשם באמצעות מפתחות גישה
כשנרשמים לחשבון חדש באפליקציה ל-Android שמשתמשת ב-Credential Manager API, המשתמשים יכולים ליצור מפתח גישה לחשבון שלהם. מפתח הגישה הזה יאוחסן באופן מאובטח בספק פרטי הכניסה שנבחר על ידי המשתמש, וישמש לכניסות עתידיות לחשבון, בלי שהמשתמש יצטרך להזין את הסיסמה בכל פעם.
עכשיו תיצרו מפתח גישה ותרשמו את פרטי הכניסה של המשתמש על סמך המידע הביומטרי או השיטה לביטול הנעילה של המסך.
הרשמה באמצעות מפתח גישה
Inside Credential Manager -> אפליקציה -> ראשי -> Java -> SignUpFragment.kt, ניתן לראות את שדה הטקסט username. ולחצן להרשמה עם מפתח גישה.
מעבירים את האתגר ותגובת json אחרת כדי לבצע קריאה ל-createPasskey()
לפני שיוצרים מפתח גישה, צריך לבקש מהשרת לקבל את המידע הדרוש שיועבר ל-API של מנהל פרטי הכניסה במהלך הקריאה של createCredential() .
למרבה המזל, כבר יש תגובה מדומה בנכסים(RegFromServer.txt) שמחזירה פרמטרים כאלה ב-Codelab הזה.
- באפליקציה, עוברים לשיטה SignUpFragment.kt, Find, signUpWithPasskeys, שבה תכתוב את הלוגיקה ליצירת מפתח גישה ולמתן גישה למשתמש. תוכלו למצוא את השיטה באותה כיתה.
- מסמנים את הבלוק הנוסף עם הערה כדי לקרוא ל-createPasskey() ולהחליף אותו בקוד הבא :
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())
אחזור ההרשמהJsonFromServer() הוא שיטה שקוראת את תגובת ה-json של הרישום מהנכסים ומחזירה את קובץ ה-json של הרישום שיועבר במהלך יצירת מפתח הגישה.
- מחפשים את שיטת pullRegistrationJsonFromServer() ומחליפים את ה-TODO בקוד הבא כדי להחזיר json וגם להסיר את הצהרת ההחזרה של המחרוזת הריקה :
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 של הרישום מנכסים.
- בקובץ JSON הזה יש 4 שדות שצריך להחליף.
- UserId צריך להיות ייחודי כדי שמשתמש יוכל ליצור כמה מפתחות גישה (במקרה הצורך). אתם מחליפים את <userId> עם מזהה המשתמש שנוצר.
- <challenge> גם צריך להיות ייחודי, כך שתיצרו אתגר ייחודי אקראי. השיטה כבר כלולה בקוד שלכם.
קטע הקוד הבא כולל אפשרויות לדוגמה שאתם מקבלים מהשרת:
{
"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 בייטים לפחות. זהו שדה חובה אבל לא משתמשים בו במהלך הרישום, אלא אם מבצעים אימות (attestation). | |
המזהה הייחודי של המשתמש. אסור שהערך הזה יכלול פרטים אישיים מזהים, כמו כתובות אימייל או שמות משתמשים. ערך אקראי של 16 בייט שנוצר לכל חשבון יפעל היטב. | |
השדה הזה צריך לכלול מזהה ייחודי של החשבון שהמשתמש יזהה, כמו כתובת האימייל או שם המשתמש שלו. החשבון הזה יוצג בבורר החשבונות. (אם משתמשים בשם משתמש, יש להשתמש באותו ערך כמו באימות הסיסמה). | |
השדה הזה הוא שם אופציונלי וידידותי יותר למשתמש. זהו שם תקין לחשבון המשתמש והוא מיועד לתצוגה בלבד. | |
הישות הנסמך תואמת לפרטי הבקשה שלך.היא צריכה :
| |
הפרמטרים של פרטי הכניסה למפתח הציבורי הם רשימה של אלגוריתמים וסוגי מפתחות מותרים. הרשימה חייבת להכיל לפחות רכיב אחד. | |
יכול להיות שהמשתמש שמנסה לרשום מכשיר רשם מכשירים אחרים. כדי להגביל יצירה של פרטי כניסה מרובים לאותו חשבון במאמת יחיד, אתם יכולים להתעלם מהמכשירים האלה. אם צוין אחרת, חברת transports צריכה להכיל את התוצאה של הקריאה ל-getTransports() במהלך הרישום של כל פרטי כניסה. | |
מציין אם המכשיר צריך להיות מחובר לפלטפורמה או לא, או אם אין דרישה לגביה. מגדירים אותו כ-platform. הכוונה היא שאנחנו רוצים לאמת שמוטמע במכשיר הפלטפורמה, והמשתמש לא יקבל בקשה להוסיף.לדוגמה. מפתח אבטחה בחיבור USB. | |
| לציין את הערך 'חובה'. כדי ליצור מפתח גישה. |
יצירת פרטי כניסה
- אחרי שיוצרים פונקציית 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 Studio או פקודת adb לניפוי באגים
- עכשיו, בסוף צריך להשלים את תהליך הרישום על ידי שליחת פרטי הכניסה של המפתח הציבורי לשרת ומתן אפשרות למשתמש להיכנס. האפליקציה מקבלת אובייקט של פרטי כניסה שמכיל מפתח ציבורי שאפשר לשלוח לשרת כדי לרשום את מפתח הגישה.
כאן השתמשנו בשרת מדומה, לכן אנחנו פשוט מחזירים את הערך true שמציין שהשרת שמר את המפתח הציבורי הרשום למטרות אימות ואימות עתידיים.
בשיטת Inside 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()
}
- signResponse מחזיר true שמציין (mock) שמר את המפתח הציבורי לשימוש עתידי.
- הסימון של מהדורת ה-SignInthroughPasskeys הוא True, כדי לציין שאתם מתחברים באמצעות מפתחות גישה.
- לאחר ההתחברות, מפנים את המשתמשים למסך הבית.
קטע הקוד הבא מכיל כמה אפשרויות לדוגמה שאתם אמורים לקבל:
{
"id": String,
"rawId": String,
"type": "public-key",
"response": {
"clientDataJSON": String,
"attestationObject": String,
}
}
הטבלה הבאה היא חלקית, אבל היא מכילה את הפרמטרים החשובים בפונקציה PublicKeyCredential
:
פרמטרים | תיאורים |
מזהה של מפתח הגישה שנוצר בקידוד Base64URL. המזהה הזה עוזר לדפדפן לקבוע אם יש במכשיר מפתח גישה תואם במהלך האימות. יש לשמור את הערך הזה במסד הנתונים בקצה העורפי. | |
גרסת אובייקט | |
נתוני לקוח בקידוד של אובייקט | |
אובייקט אימות (attestation) בקידוד |
אחרי שמפעילים את האפליקציה, אפשר ללחוץ על הלחצן 'הרשמה באמצעות מפתחות גישה' וליצור מפתח גישה.
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(request, requireActivity()) as CreatePasswordResponse
} catch (e: Exception) {
Log.e("Auth", " Exception Message : " + e.message)
}
- עכשיו שמרת את פרטי הכניסה לסיסמה בספק הסיסמה של המשתמש, כדי לבצע אימות באמצעות סיסמה בהקשה אחת בלבד.
5. הוספת אפשרות לאימות באמצעות מפתח גישה או סיסמה
עכשיו אתם מוכנים להשתמש בה כדי לבצע אימות באפליקציה בצורה בטוחה.
מקבלים את האתגר ואפשרויות נוספות שצריך להעביר כדי להפעיל את getPasskey()
לפני שמבקשים מהמשתמש לבצע אימות, צריך לבקש מהשרת פרמטרים שיועברו ב-WebAuthn json, כולל אתגר.
כבר יש תגובה מדומה בנכסים(AuthFromServer.txt) שמחזירה פרמטרים כאלה ב-Codelab הזה.
- באפליקציה, עוברים אל SignInFragment.kt, מחפשים את השיטה
signInWithSavedCredentials
שבה צריך לכתוב את הלוגיקה לאימות באמצעות סיסמה או מפתח גישה שנשמרו, ולאפשר למשתמש לבצע את הפעולות הבאות : - מסמנים את הבלוק הנוסף עם הערה כדי לקרוא ל-createPasskey() ולהחליף אותו בקוד הבא :
SignInFragment.kt
//TODO : Call getSavedCredentials() method to signin using passkey/password
val data = getSavedCredentials()
- בתוך ה-method getSavedCredentials() , צריך ליצור
GetPublicKeyCredentialOption
() עם הפרמטרים הנדרשים כדי לקבל את פרטי הכניסה מהספק של פרטי הכניסה.
SigninFragment.kt
//TODO create a GetPublicKeyCredentialOption() with necessary registration json from server
val getPublicKeyCredentialOption =
GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null)
אחזור AuthJsonFromServer() הזה הוא שיטה, שקוראת את תגובת ה-json לאימות מהנכסים ומחזירה את ה-JSON לאימות כדי לאחזר את כל מפתחות הגישה שמשויכים לחשבון המשתמש הזה.
הפרמטר השני : clientDataHash - גיבוב המשמש לאימות הזהות של הצד הנסמך, שמוגדר רק אם הגדרת את GetCredentialRequest.origin. באפליקציה לדוגמה, הערך הוא null.
הפרמטר השלישי הוא True אם אתם מעדיפים שהפעולה תוחזר באופן מיידי כשאין פרטי כניסה זמינים במקום לחזור אחורה כדי לגלות פרטי כניסה מרחוק. אחרת, הערך הוא False (ברירת המחדל).
- מחפשים את שיטתFetchAuthJsonFromServer() ומחליפים את ה-TODO בקוד הבא כדי להחזיר json, וגם להסיר את הצהרת ההחזרה של המחרוזת הריקה :
SignInFragment.kt
//TODO fetch authentication mock json
return requireContext().readFromAsset("AuthFromServer")
הערה : השרת של ה-Codelab הזה תוכנן להחזיר קובץ JSON שדומה ככל האפשר למילון PublicKeyCredentialRequestOptions
שמועבר לקריאה מסוג getCredential() של API. קטע הקוד הבא כולל כמה אפשרויות לדוגמה שאתם אמורים לקבל:
{
"challenge": String,
"rpId": String,
"userVerification": "",
"timeout": 1800000
}
הטבלה הבאה היא חלקית, אבל היא מכילה את הפרמטרים החשובים במילון PublicKeyCredentialRequestOptions
:
פרמטרים | תיאורים |
אתגר שנוצר על ידי שרת באובייקט | |
מזהה RP הוא דומיין. אתר יכול לציין את הדומיין שלו או סיומת שניתנת לרישום. הערך הזה צריך להתאים לפרמטר |
- בשלב הבא צריך ליצור אובייקט 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(). הפעולה הזו אורכת את רשימת האפשרויות של פרטי הכניסה ואת ההקשר של הפעילות כדי להציג את האפשרויות בגיליון התחתון בהקשר הזה.
- אחרי שהבקשה תאושר, יופיע במסך גיליון תחתון עם כל פרטי הכניסה שנוצרו לחשבון המשויך.
- עכשיו המשתמשים יכולים לאמת את הזהות שלהם באמצעות מידע ביומטרי או נעילת מסך וכו', כדי לאמת את פרטי הכניסה שנבחרו.
- הסימון של מהדורת ה-SignInthroughPasskeys הוא True, כדי לציין שאתם מתחברים באמצעות מפתחות גישה. אחרת, הערך יהיה False.
- אתם תטפלו בנראות של התצוגות המעובדות ותטפלו בחריגות אם הבקשה תיכשל או תיכשל מסיבה כלשהי. כאן הודעות השגיאה נרשמות ומוצגות באפליקציה בתיבת דו-שיח של שגיאה. אפשר לבדוק את יומני השגיאות המלאים דרך Android Studio או פקודת adb לניפוי באגים
- עכשיו, בסוף צריך להשלים את תהליך הרישום על ידי שליחת פרטי הכניסה של המפתח הציבורי לשרת ומתן אפשרות למשתמש להיכנס. האפליקציה מקבלת אובייקט של פרטי כניסה שמכיל מפתח ציבורי שאפשר לשלוח לשרת כדי לבצע אימות באמצעות מפתח הגישה.
כאן השתמשנו בשרת מדומה, אז אנחנו מחזירים את הערך 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 שמציין (mock) השרת אימת את המפתח הציבורי לשימוש עתידי.
- לאחר ההתחברות, מפנים את המשתמשים למסך הבית.
קטע הקוד הבא כולל אובייקט PublicKeyCredential
לדוגמה:
{
"id": String
"rawId": String
"type": "public-key",
"response": {
"clientDataJSON": String
"authenticatorData": String
"signature": String
"userHandle": String
}
}
הטבלה הבאה היא חלקית, אבל היא מכילה את הפרמטרים החשובים באובייקט PublicKeyCredential
:
פרמטרים | תיאורים |
המזהה בקידוד Base64URL של פרטי הכניסה המאומתים של מפתח הגישה. | |
גרסת אובייקט | |
אובייקט | |
אובייקט | |
אובייקט | |
אובייקט |
הריצו את האפליקציה, מנווטים לכניסה -> נכנסים באמצעות מפתחות גישה או סיסמה שמורה ומנסים להיכנס באמצעות פרטי הכניסה שנשמרו.
רוצה לנסות?
הטמעת באפליקציה ל-Android יצירה של מפתחות גישה, שמירת סיסמה ב'מנהל פרטי הכניסה' ואימות באמצעות מפתחות גישה או סיסמה שמורה.
6. מעולה!
סיימת את ה-Codelab הזה! אם רוצים לבדוק את הרזולוציה הסופית, היא זמינה בכתובת https://github.com/android/identity-samples/tree/main/CredentialManager
אם יש לכם שאלות, אפשר לשאול אותן ב-StackOverflow עם תג passkey
.