1. לפני שמתחילים
פתרונות אימות מסורתיים גורמים למספר בעיות שקשורות לאבטחה ולנוחות השימוש.
סיסמאות נפוצות מאוד, אבל...
- קל לשכוח אותם
- המשתמשים צריכים לדעת איך יוצרים סיסמאות חזקות.
- קל לפורץ, לאסוף ולשדר מחדש על ידי תוקפים.
הצוות של Android עבד על יצירת Credential Manager 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. להגדרה
- מעתיקים את המאגר הזה למחשב הנייד מההסתעפות 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, המשתמשים יכולים ליצור מפתח גישה לחשבון שלהם. מפתח הגישה הזה יישמר באופן מאובטח אצל ספק פרטי הכניסה שבחר המשתמש, וישמש אותו להתחברות עתידית ללא צורך להזין את הסיסמה בכל פעם.
עכשיו עליכם ליצור מפתח גישה ולרשום את פרטי הכניסה של המשתמש באמצעות זיהוי ביומטרי או נעילת מסך.
הרשמה באמצעות מפתח גישה
הקוד בתוך Credential Manager/app/main/java/SignUpFragment.kt מגדיר שדה טקסט 'username' ולחצן להרשמה באמצעות מפתח גישה.
מעבירים את האתגר ואת תגובת ה-JSON האחרת לקריאה של createPasskey()
לפני שיוצרים מפתח גישה, צריך לבקש מהשרת את המידע הנדרש כדי להעביר אותו ל-Credential Manager API במהלך הקריאה createCredential().
כבר יש לכם תגובה מדומה בנכסים של הפרויקט, שנקראת RegFromServer.txt, שמחזירה את הפרמטרים הנדרשים ב-codelab הזה.
- באפליקציה, עוברים אל SignUpFragment.kt, מחפשים את השיטה signUpWithPasskeys שבה כותבים את הלוגיקה ליצירת מפתח גישה ולכניסת המשתמש. אפשר למצוא את השיטה באותה כיתה.
- בודקים את הבלוק else עם תגובה כדי לקרוא ל-
createPasskey()
ומחליפים אותו בקוד הבא:
SignUpFragment.kt
//TODO : Call createPasskey() to signup with passkey
val data = createPasskey()
ה-method הזה ייקרא אחרי שתזינו שם משתמש תקין במסך.
- בתוך השיטה
createPasskey()
, צריך ליצורCreatePublicKeyCredentialRequest()
עם הפרמטרים הנדרשים שמוחזרים.
SignUpFragment.kt
//TODO create a CreatePublicKeyCredentialRequest() with necessary registration json from server
val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer())
השיטה fetchRegistrationJsonFromServer()
קוראת תגובת JSON של שרת PublicKeyCredentialCreationOptions
מהנכסים ומחזירה את קובץ ה-JSON של הרישום שצריך להעביר בזמן יצירת מפתח הגישה.
- מחפשים את השיטה
fetchRegistrationJsonFromServer()
ומחליפים את 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 הזה חלקי ויש בו 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 בייטים. השדה הזה נדרש, אבל לא נעשה בו שימוש במהלך הרישום, אלא אם מבצעים אימות. | |
המזהה הייחודי של המשתמש. הערך הזה לא יכול לכלול פרטים אישיים מזהים, למשל כתובות אימייל או שמות משתמשים. ערך אקראי באורך 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()
. - לאחר שהבקשה תאושר, יוצג במסך גיליון תחתון עם בקשה ליצור מפתח גישה.
- עכשיו המשתמשים יכולים לאמת את הזהות שלהם באמצעות נתונים ביומטריים או נעילת מסך וכו'.
- אתם מטפלים בחשיפה של התצוגות המוצגות (rendered) ובחריגות אם הבקשה נכשלת או לא מצליחה מסיבה כלשהי. כאן מתועדות הודעות השגיאה ומוצגות באפליקציה בתיבת דו-שיח עם הודעת שגיאה. אפשר לבדוק את יומני השגיאות המלאים דרך Android Studio או באמצעות הפקודה
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 של מפתח הגישה שנוצר. המזהה הזה עוזר לדפדפן לקבוע אם מפתח גישה תואם נמצא במכשיר במהלך האימות. צריך לאחסן את הערך הזה במסד הנתונים בקצה העורפי. | |
גרסה של אובייקט | |
נתוני לקוח שמקודדים באובייקט | |
אובייקט אימות בקידוד |
מריצים את האפליקציה, לוחצים על הלחצן Sign up with passkeys (הרשמה באמצעות מפתחות גישה) ויוצרים מפתח גישה.
4. שמירת סיסמה בספק פרטי הכניסה
באפליקציה הזו, במסך SignUp (הרשמה), כבר הופעלה הרשמה עם שם משתמש וסיסמה למטרות הדגמה.
כדי לשמור את פרטי הכניסה של הסיסמה של המשתמש אצל ספק הסיסמאות שלו, צריך להטמיע 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()
לפני שמבקשים מהמשתמש לבצע אימות, צריך לבקש פרמטרים להעברה ב-JSON של WebAuthn מהשרת, כולל אתגר.
כבר יש לכם תגובה מדומה בנכסים (AuthFromServer.txt) שמחזירה פרמטרים כאלה ב-codelab הזה.
- באפליקציה, עוברים אל SignInFragment.kt, מחפשים את השיטה
signInWithSavedCredentials
שבה כותבים את הלוגיקה לאימות באמצעות מפתח גישה או סיסמה שמורים, ומאפשרים למשתמש להיכנס: - בודקים את הבלוק else עם תגובה כדי לקרוא ל-
createPasskey()
ומחליפים אותו בקוד הבא:
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
– גיבוב שמשמש לאימות הזהות של הצד הנסמך. מגדירים את השדה הזה רק אם הגדרתם את GetCredentialRequest.origin
. באפליקציית הדוגמה, הערך מוגדר כ-null
.
- מחפשים את השיטה fetchAuthJsonFromServer() ומחליפים את TODO בקוד הבא כדי להחזיר JSON, וגם מסירים את משפט ההחזרה של המחרוזת הריקה:
SignInFragment.kt
//TODO fetch authentication mock json
return requireContext().readFromAsset("AuthFromServer")
הערה : השרת של סדנת הקוד הזו מתוכנן להחזיר קובץ 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()
. המערכת משתמשת ברשימת האפשרויות של פרטי הכניסה ובהקשר של הפעילות כדי להציג את האפשרויות בגיליון התחתון בהקשר הזה. - לאחר שהבקשה תאושר, יופיע בחלק התחתון של המסך גיליון שבו יפורטו כל פרטי הכניסה שנוצרו לחשבון המשויך.
- עכשיו המשתמשים יכולים לאמת את הזהות שלהם באמצעות מידע ביומטרי או שיטה לביטול נעילת המסך וכו' כדי לאמת את פרטי הכניסה שנבחרו.
- אם פרטי הכניסה שנבחרו הם
PublicKeyCredential
, מגדירים את הדגלsetSignedInThroughPasskeys
כ-true
. אחרת, מגדירים אותו כ-false
.
קטע הקוד הבא כולל אובייקט PublicKeyCredential
לדוגמה:
{
"id": String
"rawId": String
"type": "public-key",
"response": {
"clientDataJSON": String
"authenticatorData": String
"signature": String
"userHandle": String
}
}
הטבלה הבאה לא מקיפה, אבל היא מכילה את הפרמטרים החשובים באובייקט PublicKeyCredential
:
פרמטרים | תיאורים |
המזהה בקידוד Base64URL של פרטי הכניסה המאומתים של מפתח הגישה. | |
גרסה של אובייקט | |
אובייקט | |
אובייקט | |
אובייקט | |
אובייקט |
- לבסוף, צריך להשלים את תהליך האימות. בדרך כלל, אחרי שהמשתמש משלים את אימות מפתח הגישה, האפליקציה שולחת פרטי כניסה של מפתח ציבורי שמכילים טענת נכוֹנוּת (assertion) לאימות לשרת, שמאמת את טענת הנכוֹנוּת ומאמת את המשתמש.
כאן השתמשנו בשרת מדומה, ולכן אנחנו פשוט מחזירים את הערך 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, שמציין שהשרת (הבדיוני) אימת את המפתח הציבורי לשימוש עתידי. - אחרי שהמשתמש מתחבר, מפנים אותו למסך הבית.
מריצים את האפליקציה ועוברים אל כניסה > כניסה באמצעות מפתחות גישה/סיסמה שמורה,ומנסים להיכנס באמצעות פרטי הכניסה השמורים.
רוצה לנסות?
הטמעתם את היצירה של מפתחות גישה, שמירת סיסמה ב-Credential Manager ואימות באמצעות מפתחות גישה או סיסמה שמורה באמצעות Credential Manager API באפליקציה ל-Android.
6. מעולה!
סיימת את הקודלאב הזה! אפשר לבדוק את הפתרון הסופי בכתובת https://github.com/android/identity-samples/tree/main/CredentialManager
אם יש לכם שאלות, אתם יכולים לפרסם אותן ב-StackOverflow עם תג passkey
.