כניסה לחשבון מקושר עבור Android

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

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

במסמך הזה נסביר איך לשנות את האפליקציה ל-Android כך שתתמוך בכניסה לחשבון מקושר.

איך זה עובד

  1. אתם מביעים הסכמה להצגת החשבונות המקושרים במהלך 'כניסה בהקשה אחת'.
  2. אם המשתמש מחובר ל-Google וקישר את חשבון Google שלו לחשבון בשירות שלכם, יוחזר אסימון מזהה לחשבון המקושר.
  3. למשתמש מוצגת בקשת כניסה בהקשה אחת, עם אפשרות להיכנס לשירות שלכם באמצעות החשבון המקושר.
  4. אם המשתמש יבחר להמשיך עם החשבון המקושר, האסימון המזהה שלו יוחזר לאפליקציה שלכם. תוכלו להשוות אותו לאסימון שנשלח לשרת שלכם בשלב 2 כדי לזהות את המשתמש המחובר.

הגדרה

הגדרת סביבת הפיתוח

אפשר לקבל את העדכונים האחרונים של Google Play Services במארח הפיתוח:

  1. פותחים את Android SDK Manager.
  1. בקטע כלי SDK, מאתרים את שירותי Google Play.

  2. אם הסטטוס של החבילות האלה לא מותקן, בוחרים את שתיהן ולוחצים על Install Packages.

הגדרת האפליקציה

  1. בקובץ build.gradle ברמת הפרויקט, יש לכלול את מאגר Maven של Google בקטע buildscript וגם בקטע allprojects.

    buildscript {
        repositories {
            google()
        }
    }
    
    allprojects {
        repositories {
            google()
        }
    }
    
  2. מוסיפים את יחסי התלות של ה-API "Link with Google" לקובץ ההדרגתיות של המודול ברמת האפליקציה, שהוא בדרך כלל app/build.gradle:

    dependencies {
      implementation 'com.google.android.gms:play-services-auth:21.0.0'
    }
    

איך לשנות את אפליקציית Android שלכם כך שתתמוך בכניסה לחשבון מקושר

בסוף התהליך של הכניסה לחשבון מקושר, מוחזר לאפליקציה אסימון מזהה. צריך לבדוק את התקינות של האסימון המזהה לפני הכניסה של המשתמש.

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

  1. צריך ליצור פעילות כדי לקבל את התוצאה של כוונת הכניסה

    Kotlin

      private val activityResultLauncher = registerForActivityResult(
        ActivityResultContracts.StartIntentSenderForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
          try {
            val signInCredentials = Identity.signInClient(this)
                                    .signInCredentialFromIntent(result.data)
            // Review the Verify the integrity of the ID token section for
            // details on how to verify the ID token
            verifyIdToken(signInCredential.googleIdToken)
          } catch (e: ApiException) {
            Log.e(TAG, "Sign-in failed with error code:", e)
          }
        } else {
          Log.e(TAG, "Sign-in failed")
        }
      }
    

    Java

      private final ActivityResultLauncher<IntentSenderResult>
        activityResultLauncher = registerForActivityResult(
        new ActivityResultContracts.StartIntentSenderForResult(),
        result -> {
        If (result.getResultCode() == RESULT_OK) {
            try {
              SignInCredential signInCredential = Identity.getSignInClient(this)
                             .getSignInCredentialFromIntent(result.getData());
              verifyIdToken(signInCredential.getGoogleIdToken());
            } catch (e: ApiException ) {
              Log.e(TAG, "Sign-in failed with error:", e)
            }
        } else {
            Log.e(TAG, "Sign-in failed")
        }
    });
    
  2. יצירה של בקשת הכניסה

    Kotlin

    private val tokenRequestOptions =
    GoogleIdTokenRequestOptions.Builder()
      .supported(true)
      // Your server's client ID, not your Android client ID.
      .serverClientId(getString("your-server-client-id")
      .filterByAuthorizedAccounts(true)
      .associateLinkedAccounts("service-id-of-and-defined-by-developer",
                               scopes)
      .build()
    

    Java

     private final GoogleIdTokenRequestOptions tokenRequestOptions =
         GoogleIdTokenRequestOptions.Builder()
      .setSupported(true)
      .setServerClientId("your-service-client-id")
      .setFilterByAuthorizedAccounts(true)
      .associateLinkedAccounts("service-id-of-and-defined-by-developer",
                                scopes)
      .build()
    
  3. הפעלת ה-Intent 'בהמתנה לכניסה'

    Kotlin

     Identity.signInClient(this)
        .beginSignIn(
      BeginSignInRequest.Builder()
        .googleIdTokenRequestOptions(tokenRequestOptions)
      .build())
        .addOnSuccessListener{result ->
          activityResultLauncher.launch(result.pendingIntent.intentSender)
      }
      .addOnFailureListener {e ->
        Log.e(TAG, "Sign-in failed because:", e)
      }
    

    Java

     Identity.getSignInClient(this)
      .beginSignIn(
        BeginSignInRequest.Builder()
          .setGoogleIdTokenRequestOptions(tokenRequestOptions)
          .build())
      .addOnSuccessListener(result -> {
        activityResultLauncher.launch(
            result.getPendingIntent().getIntentSender());
    })
    .addOnFailureListener(e -> {
      Log.e(TAG, "Sign-in failed because:", e);
    });
    

אימות התקינות של האסימון המזהה

כדי לוודא שהאסימון תקין, צריך לוודא שהקריטריונים הבאים מתקיימים:

  • Google חתומה באופן תקין על האסימון המזהה. יש להשתמש במפתחות הציבוריים של Google (זמינים בפורמט JWK או PEM) לאימות החתימה של האסימון. המפתחות האלה עוברים רוטציה באופן קבוע. כדאי לבדוק את הכותרת Cache-Control בתשובה כדי לקבוע מתי צריך לאחזר אותם שוב.
  • הערך של aud באסימון המזהה שווה לאחד ממזהי הלקוח של האפליקציה שלך. הבדיקה הזו נדרשת כדי למנוע אסימונים מזהים שהונפקו לאפליקציה זדונית שנעשה בה שימוש כדי לגשת לנתונים של אותו משתמש בשרת הקצה העורפי של האפליקציה.
  • הערך של iss באסימון המזהה שווה ל-accounts.google.com או ל-https://accounts.google.com.
  • מועד התפוגה (exp) של האסימון המזהה לא חלף.
  • כדי לוודא שהאסימון המזהה מייצג חשבון ארגוני ב-Google Workspace או ב-Cloud, אפשר לבדוק את ההצהרה hd, שבה מצוין הדומיין המתארח של המשתמש. צריך להשתמש באפשרות הזו כשמגבילים את הגישה למשאב רק לחברים בדומיינים מסוימים. אם לא ההצהרה הזו לא תשויך, החשבון לא שייך לדומיין באירוח של Google.

במקום לכתוב קוד משלך לביצוע שלבי האימות האלה, מומלץ מאוד להשתמש בספריית לקוח של Google API לפלטפורמה שלך, או בספריית JWT לשימוש כללי. לפיתוח ולניפוי באגים, אפשר לקרוא לנקודת הקצה של האימות tokeninfo.

שימוש בספריית לקוח של Google API

הדרך המומלצת לאימות אסימונים מזהים של Google בסביבת ייצור היא באמצעות ספריית הלקוח של Java Google API.

Java

  import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
  import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
  import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;

  ...

  GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
      // Specify the CLIENT_ID of the app that accesses the backend:
      .setAudience(Collections.singletonList(CLIENT_ID))
      // Or, if multiple clients access the backend:
      //.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
      .build();

  // (Receive idTokenString by HTTPS POST)

  GoogleIdToken idToken = verifier.verify(idTokenString);
  if (idToken != null) {
    Payload payload = idToken.getPayload();

    // Print user identifier
    String userId = payload.getSubject();
    System.out.println("User ID: " + userId);

    // Get profile information from payload
    String email = payload.getEmail();
    boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
    String name = (String) payload.get("name");
    String pictureUrl = (String) payload.get("picture");
    String locale = (String) payload.get("locale");
    String familyName = (String) payload.get("family_name");
    String givenName = (String) payload.get("given_name");

    // Use or store profile information
    // ...

  } else {
    System.out.println("Invalid ID token.");
  }

השיטה GoogleIdTokenVerifier.verify() מאמתת את חתימת ה-JWT, את הצהרת aud, את הצהרת iss ואת הצהרת הבעלות שלהםexp.

אם אתם צריכים לוודא שהאסימון המזהה מייצג חשבון Google Workspace או חשבון ארגוני ב-Cloud, תוכלו לאמת את ההצהרה hd על ידי בדיקת שם הדומיין שהוחזר על ידי השיטה Payload.getHostedDomain().

מתבצעת קריאה לנקודת הקצה של פרטי האסימון

דרך קלה לאמת חתימה של אסימון מזהה לצורך ניפוי באגים היא להשתמש בנקודת הקצה tokeninfo. הפעלה של נקודת הקצה הזו כרוכה בבקשת רשת נוספת שמבצעת את רוב תהליך האימות בזמן הבדיקה של האימות והחילוץ של המטען הייעודי (payload) בקוד שלך. היא לא מתאימה לשימוש בקוד של סביבת הייצור כי הבקשות עשויות להיות מווסתות או שעשויות לכלול שגיאות מדי פעם.

כדי לאמת אסימון מזהה באמצעות נקודת הקצה של tokeninfo, יש לשלוח בקשת HTTPS POST או GET לנקודת הקצה, ולהעביר את האסימון המזהה בפרמטר id_token. לדוגמה, כדי לאמת את האסימון "XYZ123", יש לשלוח את בקשת GET הבאה:

https://oauth2.googleapis.com/tokeninfo?id_token=XYZ123

אם האסימון נחתם כראוי וההצהרות של iss ו-exp כוללות את הערכים הצפויים, תתקבל תגובת HTTP 200, שבה הגוף מכיל את ההצהרות על אסימונים בפורמט JSON. זוהי דוגמה לתשובה:

{
 // These six fields are included in all Google ID Tokens.
 "iss": "https://accounts.google.com",
 "sub": "110169484474386276334",
 "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "iat": "1433978353",
 "exp": "1433981953",

 // These seven fields are only included when the user has granted the "profile" and
 // "email" OAuth scopes to the application.
 "email": "testuser@gmail.com",
 "email_verified": "true",
 "name" : "Test User",
 "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
 "given_name": "Test",
 "family_name": "User",
 "locale": "en"
}

כדי לוודא שהאסימון המזהה מייצג חשבון Google Workspace, אפשר לבדוק את ההצהרה hd, המציינת את הדומיין המתארח של המשתמש. צריך להשתמש באפשרות הזו כשמגבילים את הגישה למשאב רק לחברים בדומיינים מסוימים. אם לא ההצהרה הזו לא תיכלל, המשמעות היא שהחשבון לא שייך לדומיין מתארח ב-Google Workspace.