ממשק ה-API הראשון שלך מסוג Android FIDO2

1. מבוא

מה זה FIDO2 API?

ה-API ל-FIDO2 מאפשר לאפליקציות ל-Android ליצור פרטי כניסה מבוססי-מפתח ציבורי מאומתים ולהשתמש בהם לצורך אימות משתמשים. ה-API מספק הטמעה של לקוח WebAuthn, שתומך בשימוש במאמתי אימות של BLE , NFC ו-USB בנדידה (מפתחות אבטחה), וגם בכלי לאימות פלטפורמה שמאפשר למשתמשים לבצע אימות באמצעות טביעת האצבע או נעילת המסך.

מה תפַתחו...

ב-Codelab הזה, ברצונך לפתח אפליקציה ל-Android עם פונקציונליות פשוטה של אימות מחדש באמצעות חיישן טביעות אצבע. 'אימות מחדש' הוא כשמשתמש נכנס לאפליקציה, ולאחר מכן מבצע אימות מחדש כשהוא חוזר לאפליקציה, או כשהוא מנסה לגשת לקטע חשוב באפליקציה. המקרה השני נקרא גם 'אימות בשלבים'.

מה תלמדו...

במאמר זה תלמדו איך לקרוא ל-Android FIDO2 API ומה ניתן לעשות כדי להתאים לאירועים שונים. תלמדו גם אימות מחדש של שיטות מומלצות ספציפיות.

מה צריך...

  • מכשיר Android עם חיישן טביעות אצבע (גם בלי חיישן טביעות אצבע, נעילת המסך יכולה לספק פונקציונליות מקבילה של אימות המשתמש)
  • Android OS 7.0 ואילך עם העדכונים האחרונים. הקפידו לרשום טביעת אצבע (או נעילת מסך).

2. בתהליך ההגדרה

שכפול המאגר

כדאי לבדוק את המאגר ב-GitHub.

https://github.com/android/codelab-fido2

$ git clone https://github.com/android/codelab-fido2.git

מה אנחנו מתכוונים ליישם?

  • מתן הרשאה למשתמשים לרשום 'מאמת פלטפורמה של משתמש שמאמת אותו' (טלפון Android עם חיישן טביעות האצבע עצמו יפעל כמכשיר אחד).
  • לאפשר למשתמשים לאמת את עצמם מחדש באפליקציה באמצעות טביעת האצבע שלהם.

כאן אפשר לראות תצוגה מקדימה של מה שמתכננים לפתח.

התחלת הפרויקט ב-Codelab

האפליקציה שהושלמה שולחת בקשות לשרת בכתובת https://webauthn-codelab.glitch.me. אפשר לנסות שם את גרסת האינטרנט של אותה האפליקציה.

c2234c42ba8a6ef1.png

בחרת לעבוד על גרסה משלך של האפליקציה.

  1. עוברים אל דף העריכה של האתר בכתובת https://glitch.com/edit/#!/webauthn-codelab.
  2. חיפוש של 'רמיקס לעריכה' בפינה הימנית העליונה. אפשר ללחוץ על הלחצן את הקוד ולהמשיך עם גרסה משלכם עם כתובת URL חדשה של פרויקט. 9ef108869885e4ce.png
  3. מעתיקים את שם הפרויקט בפינה הימנית העליונה (אפשר לשנות אותו לפי הצורך). c91d0d59c61021a4.png
  4. מדביקים אותו בקטע HOSTNAME של הקובץ .env בתקלה. 889b55b1cf74b894.png

3. שיוך האפליקציה והאתר לקישורים לנכסים דיגיטליים

כדי להשתמש ב-FIDO2 API באפליקציה ל-Android, צריך לשייך אותה לאתר ולשתף את פרטי הכניסה ביניהם. כדי לעשות את זה, משתמשים בקישורים לנכסים דיגיטליים. כדי להצהיר על שיוכים, אפשר לארח קובץ JSON של Digital Asset Links באתר ולהוסיף קישור לקובץ Digital Asset Link למניפסט של האפליקציה.

מארח את .well-known/assetlinks.json בדומיין שלך

אפשר להגדיר שיוך בין האפליקציה לבין האתר על ידי יצירת קובץ JSON ולשייך אותו ב-.well-known/assetlinks.json. למרבה המזל, יש לנו קוד שרת שמציג את הקובץ assetlinks.json באופן אוטומטי, פשוט על ידי הוספת הפרמטרים הבאים של הסביבה לקובץ .env בתקלה:

  • ANDROID_PACKAGENAME: שם החבילה של האפליקציה (com.example.android.fido2)
  • ANDROID_SHA256HASH: גיבוב SHA256 של אישור החתימה

כדי לקבל את הגיבוב SHA256 של אישור החתימה על המפתח, צריך להשתמש בפקודה שלמטה. סיסמת ברירת המחדל למאגר המפתחות של ניפוי הבאגים היא 'android'.

$ keytool -exportcert -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore

בגישה אל https://<your-project-name>.glitch.me/.well-known/assetlinks.json , אתם אמורים לראות מחרוזת JSON כמו:

[{
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "web",
    "site": "https://<your-project-name>.glitch.me"
  }
}, {
  "relation": ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.android.fido2",
    "sha256_cert_fingerprints": ["DE:AD:BE:EF:..."]
  }
}]

פתיחת הפרויקט ב-Android Studio

לוחצים על 'פתיחת פרויקט Android Studio קיים' במסך הפתיחה של Android Studio.

בחירה ב-Android שבמאגר.

1062875cf11ffb95.png

שיוך האפליקציה לרמיקס

פתיחת הקובץ gradle.properties. בתחתית הקובץ, משנים את כתובת ה-URL של המארח לרמיקס שיצרתם ב-Glitch.

// ...

# The URL of the server
host=https://<your-project-name>.glitch.me

בשלב הזה, ההגדרות האישיות של Digital Asset Links כבר מוכנות.

4. איך האפליקציה פועלת עכשיו

עכשיו נראה איך האפליקציה עובדת. עליך להקפיד לבחור באפשרות 'app-start' בתיבה המשולבת של הרצת ההגדרות. לוחצים על 'הפעלה'. (המשולש הירוק שלצד התיבה המשולבת) כדי להפעיל את האפליקציה במכשיר Android המחובר.

29351fb97062b43c.png

כשתפעילו את האפליקציה, תראו את המסך שבו תוכלו להקליד את שם המשתמש שלכם. This is UsernameFragment. לצורך ההדגמה, האפליקציה והשרת מקבלים כל שם משתמש. פשוט מקלידים משהו ומקישים על 'הבא'.

bd9007614a9a3644.png

המסך הבא שיוצג הוא AuthFragment. כאן המשתמש יכול להיכנס באמצעות סיסמה. בהמשך נוסיף תכונה לכניסה באמצעות FIDO2 כאן. שוב, לצורך ההדגמה, האפליקציה והשרת מקבלים כל סיסמה. פשוט מקלידים משהו ולוחצים על 'כניסה'.

d9caba817a0a99bd.png

זהו המסך האחרון של האפליקציה הזו, HomeFragment. בינתיים, תוצג כאן רק רשימה ריקה של פרטי כניסה. לחיצה על 'אימות מחדש' תחזיר אותך אל AuthFragment. הקשה על 'יציאה' תחזיר אותך אל UsernameFragment. לחצן הפעולה הצף עם הסימן "+" לא עושה שום דבר כרגע, אבל יתחיל רישום של

פרטי כניסה חדשים לאחר הטמעת תהליך הרישום FIDO2.

1cfcc6c884020e37.png

לפני שתתחילו לתכנת, הנה טכניקה שימושית. ב-Android Studio, מקישים על TODO שלמטה. תופיע רשימה של כל המשימות לביצוע משימות ב-codelab הזה. בקטע הבא נתחיל בביצוע הפעולה הראשונה.

e5a811bbc7cd7b30.png

5. רישום פרטי כניסה באמצעות טביעת אצבע

כדי להפעיל אימות באמצעות טביעת אצבע, תחילה יש לרשום פרטי כניסה שנוצרו על ידי משתמש שמאמת את מאמת הפלטפורמה – כלי אימות שהוטמע במכשיר ומאמת את המשתמש באמצעות מידע ביומטרי, כמו חיישן טביעות אצבע.

37ce78fdf2759832.png

כפי שראינו בקטע הקודם, לחצן הפעולה הצף לא עושה עכשיו כלום. עכשיו נראה איך אפשר לרשום פרטי כניסה חדשים.

קוראים ל-API של השרת: /auth/registerRequest

פותחים את AuthRepository.kt ומחפשים את TODO(1).

כאן, registerRequest היא השיטה שמופעלת כשלוחצים על לחצן ה-FAB. אנחנו רוצים להפעיל את השיטה הזו כדי לקרוא ל-API של השרת /auth/registerRequest. ה-API מחזיר ApiResult עם כל ה-PublicKeyCredentialCreationOptions שהלקוח צריך כדי ליצור פרטי כניסה חדשים.

לאחר מכן נוכל להתקשר אל getRegisterPendingIntent כדי להציג את האפשרויות האלה. ה-API הזה מסוג FIDO2 מחזיר את ה-PendingIntent של Android כדי לפתוח תיבת דו-שיח של טביעת אצבע וליצור פרטי כניסה חדשים, ואנחנו יכולים להחזיר את ה-PendingIntent הזה לקורא.

לאחר מכן, השיטה תיראה כך.

suspend fun registerRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    try {
      val sessionId = dataStore.read(SESSION_ID)!!
      when (val apiResult = api.registerRequest(sessionId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          if (apiResult.sessionId != null) {
            dataStore.edit { prefs ->
              prefs[SESSION_ID] = apiResult.sessionId
            }
          }
          val task = client.getRegisterPendingIntent(apiResult.data)
          return task.await()
        }
      }
    } catch (e: Exception) {
      Log.e(TAG, "Cannot call registerRequest", e)
    }
  }
  return null
}

פתיחת תיבת הדו-שיח של טביעת האצבע לרישום

פותחים את HomeFragment.kt ומחפשים את TODO(2).

כאן ממשק המשתמש מחזיר את ה-Intent מ-AuthRepository. כאן נשתמש בחבר createCredentialIntentLauncher כדי להפעיל את PendingIntent שקיבלנו כתוצאה מהשלב הקודם. תיפתח תיבת דו-שיח ליצירת פרטי כניסה.

binding.add.setOnClickListener {
  lifecycleScope.launch {
    val intent = viewModel.registerRequest()
    if (intent != null) {
      createCredentialIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
}

קבלת תוצאות הפעילות עם פרטי הכניסה החדשים

פותחים את HomeFragment.kt ומחפשים את TODO(3).

תתבצע קריאה לשיטה handleCreateCredentialResult הזו אחרי שתיבת הדו-שיח של טביעת האצבע תיסגר. אם פרטי כניסה נוצרו בהצלחה, החבר data ב-Activity result יכיל את פרטי פרטי הכניסה.

קודם כול, צריך לחלץ PublicKeyCredential מה-data. ב-Intent של הנתונים יש שדה נוסף של מערך בייטים עם המפתח Fido.FIDO2_KEY_CREDENTIAL_EXTRA. אפשר להשתמש בשיטה סטטית ב-PublicKeyCredential שנקראת deserializeFromBytes כדי להפוך את מערך הבייטים לאובייקט PublicKeyCredential.

בשלב הבא צריך לבדוק אם האיבר response של אובייקט פרטי הכניסה הזה הוא AuthenticationErrorResponse. אם כן, הייתה שגיאה ביצירת פרטי הכניסה. אחרת, נוכל לשלוח פרטי כניסה לקצה העורפי שלנו.

השיטה הסופית תיראה כך:

private fun handleCreateCredentialResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_LONG).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.credential_error, Toast.LENGTH_LONG)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_LONG)
          .show()
      } else {
        viewModel.registerResponse(credential)
      }
    }
  }
}

קוראים ל-API של השרת: /auth/registerResponse

פותחים את AuthRepository.kt ומחפשים את TODO(4).

הקריאה ל-method registerResponse היא אחרי שממשק המשתמש יצר פרטי כניסה חדשים ואנחנו רוצים לשלוח אותם חזרה לשרת.

האובייקט PublicKeyCredential מכיל מידע על פרטי הכניסה החדשים שנוצרו. עכשיו אנחנו רוצים לזכור את המזהה של המפתח המקומי שלנו כדי שנוכל להבדיל בינו לבין מפתחות אחרים שרשומים בשרת. באובייקט PublicKeyCredential, לוקחים את המאפיין rawId ומציבים אותו במשתנה מחרוזת מקומית באמצעות toBase64.

עכשיו אנחנו מוכנים לשלוח את המידע לשרת. משתמשים ב-api.registerResponse כדי לקרוא ל-API של השרת ולשלוח את התשובה בחזרה. הערך המוחזר מכיל רשימה של כל פרטי הכניסה שנרשמו בשרת, כולל פרטי הכניסה החדשים.

לסיום, אפשר לשמור את התוצאות בDataStore. רשימת פרטי הכניסה צריכה להישמר עם המפתח CREDENTIALS בתור StringSet. אפשר להשתמש ב-toStringSet כדי להמיר את רשימת פרטי הכניסה ל-StringSet.

בנוסף, אנחנו שומרים את המזהה של פרטי הכניסה עם המפתח LOCAL_CREDENTIAL_ID.

suspend fun registerResponse(credential: PublicKeyCredential) {
  try {
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.registerResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

מפעילים את האפליקציה, ואז אפשר ללחוץ על ה-FAB ולרשום פרטי כניסה חדשים.

7d64d9289c5a3cbc.png

6. אימות המשתמש באמצעות טביעת אצבע

עכשיו יש לנו פרטי כניסה שרשומים באפליקציה ובשרת. עכשיו אנחנו יכולים להשתמש בה כדי לאפשר למשתמש להיכנס לחשבון. אנחנו מוסיפים ל-AuthFragment את תכונת הכניסה בטביעת אצבע. כשמשתמש מגיע אליו, מוצגת תיבת דו-שיח של טביעת אצבע. כשהאימות יצליח, המשתמש יופנה אוטומטית לכתובת HomeFragment.

קוראים ל-API של השרת: /auth/signinRequest

פותחים את AuthRepository.kt ומחפשים את TODO(5).

מתבצעת קריאה ל-method signinRequest כשפותחים את AuthFragment. כאן אנחנו רוצים לבקש את השרת ולבדוק אם אפשר לתת למשתמש להיכנס באמצעות FIDO2.

קודם כול אנחנו צריכים לאחזר את PublicKeyCredentialRequestOptions מהשרת. משתמשים ב-api.signInRequest כדי לשלוח קריאה ל-API של השרת. הערך של ApiResult שהוחזר מכיל PublicKeyCredentialRequestOptions.

באמצעות PublicKeyCredentialRequestOptions, אנחנו יכולים להשתמש ב-FIDO2 API getSignIntent כדי ליצור PendingIntent לפתיחת תיבת הדו-שיח של טביעת האצבע.

לבסוף, אפשר להחזיר את ה-PendingIntent בחזרה לממשק המשתמש.

suspend fun signinRequest(): PendingIntent? {
  fido2ApiClient?.let { client ->
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = dataStore.read(LOCAL_CREDENTIAL_ID)
    if (credentialId != null) {
      when (val apiResult = api.signinRequest(sessionId, credentialId)) {
        ApiResult.SignedOutFromServer -> forceSignOut()
        is ApiResult.Success -> {
          val task = client.getSignPendingIntent(apiResult.data)
          return task.await()
        }
      }
    }
  }
  return null
}

פתיחת תיבת הדו-שיח של טביעת האצבע לטענת נכוֹנוּת (assertion)

פותחים את AuthFragment.kt ומחפשים את TODO(6).

זה די דומה למה שעשינו בתהליך הרישום. אנחנו יכולים להפעיל את תיבת הדו-שיח של טביעת האצבע עם המנוי signIntentLauncher.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
  launch {
    viewModel.signinRequests.collect { intent ->
      signIntentLauncher.launch(
        IntentSenderRequest.Builder(intent).build()
      )
    }
  }
  launch {
    ...
  }
}

טיפול בתוצאת הפעילות

פותחים את AuthFragment.kt ומחפשים את TODO(7).

שוב, זה בדיוק כמו שעשינו בשביל הרישום. אפשר לחלץ את PublicKeyCredential, לאתר שגיאה ולהעביר אותה ל-ViewModel.

private fun handleSignResult(activityResult: ActivityResult) {
  val bytes = activityResult.data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
  when {
    activityResult.resultCode != Activity.RESULT_OK ->
      Toast.makeText(requireContext(), R.string.cancelled, Toast.LENGTH_SHORT).show()
    bytes == null ->
      Toast.makeText(requireContext(), R.string.auth_error, Toast.LENGTH_SHORT)
        .show()
    else -> {
      val credential = PublicKeyCredential.deserializeFromBytes(bytes)
      val response = credential.response
      if (response is AuthenticatorErrorResponse) {
        Toast.makeText(requireContext(), response.errorMessage, Toast.LENGTH_SHORT)
          .show()
      } else {
        viewModel.signinResponse(credential)
      }
    }
  }
}

קוראים ל-API של השרת: /auth/signinResponse

פותחים את AuthRepository.kt ומחפשים את TODO(8).

מזהה פרטי הכניסה באובייקט PublicKeyCredential הוא keyHandle. בדיוק כמו שעשינו בתהליך הרישום, נשמור את הקוד הזה במשתנה מקומי כדי שנוכל לאחסן אותו מאוחר יותר.

עכשיו אנחנו מוכנים לשלוח קריאה ל-API של השרת באמצעות api.signinResponse. הערך המוחזר מכיל רשימה של פרטי כניסה.

בשלב הזה הכניסה לחשבון מתבצעת בהצלחה. אנחנו צריכים לשמור את כל התוצאות בDataStore שלנו. רשימת פרטי הכניסה צריכה להיות מאוחסנת כ-StringSet עם המפתח CREDENTIALS. מזהה פרטי הכניסה המקומי שנשמר למעלה צריך להיות מאוחסן כמחרוזת עם המפתח LOCAL_CREDENTIAL_ID.

לסיום, עלינו לעדכן את מצב הכניסה כדי שממשק המשתמש יוכל להפנות את המשתמש אל HomeFragment. אפשר לעשות זאת על ידי פליטת אובייקט SignInState.SignedIn ל-SharedFlow בשם signInStateMutable. אנחנו רוצים גם לקרוא ל-refreshCredentials כדי לאחזר את פרטי הכניסה של המשתמש, כך שהם יופיעו בממשק המשתמש.

suspend fun signinResponse(credential: PublicKeyCredential) {
  try {
    val username = dataStore.read(USERNAME)!!
    val sessionId = dataStore.read(SESSION_ID)!!
    val credentialId = credential.rawId.toBase64()
    when (val result = api.signinResponse(sessionId, credential)) {
      ApiResult.SignedOutFromServer -> forceSignOut()
      is ApiResult.Success -> {
        dataStore.edit { prefs ->
          result.sessionId?.let { prefs[SESSION_ID] = it }
          prefs[CREDENTIALS] = result.data.toStringSet()
          prefs[LOCAL_CREDENTIAL_ID] = credentialId
        }
        signInStateMutable.emit(SignInState.SignedIn(username))
        refreshCredentials()
      }
    }
  } catch (e: ApiException) {
    Log.e(TAG, "Cannot call registerResponse", e)
  }
}

מריצים את האפליקציה ולוחצים על 'אימות מחדש'. כדי לפתוח את AuthFragment. עכשיו אמורה להופיע תיבת דו-שיח של טביעת אצבע עם בקשה להיכנס לחשבון באמצעות טביעת האצבע.

45f81419f84952c8.png

מזל טוב! עכשיו למדת איך להשתמש ב-FIDO2 API ב-Android לצורך רישום וכניסה.

7. מעולה!

סיימתם בהצלחה את Codelab – ממשק ה-API הראשון שלך עם Android FIDO2.

מה למדת

  • איך לרשום פרטי כניסה באמצעות מאמת פלטפורמה של משתמש.
  • איך לאמת משתמש באמצעות מאמת רשום.
  • אפשרויות זמינות לרישום מאמת חשבונות חדש.
  • שיטות מומלצות של חוויית המשתמש לאימות מחדש באמצעות חיישן ביומטרי.

השלב הבא

  • לומדים איך בונים חוויה דומה באתר

כדי להתנסות בו, אתם יכולים להתנסות ב-Codelab הראשונה שלך לבדיקת WebAuthn!

משאבים

תודה מיוחדת ל-Yuriy Ackermen מ-FIDO Alliance על העזרה.