קישור חשבון באמצעות כניסה באמצעות חשבון Google (Dialogflow)

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

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

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

הטמעת קישור של חשבון כניסה באמצעות חשבון Google

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

הגדרת הפרויקט

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

  1. פותחים את Actions Console ובוחרים פרויקט.
  2. לוחצים על הכרטיסייה פיתוח ובוחרים באפשרות קישור חשבונות.
  3. מפעילים את המתג שלצד קישור חשבונות.
  4. בקטע יצירת חשבון, בוחרים באפשרות כן.
  5. בקטע סוג הקישור, בוחרים באפשרות כניסה באמצעות חשבון Google.

  6. פותחים את פרטי הלקוח ורושמים את הערך של מזהה הלקוח ש-Google סיפקה לפעולות שלכם.

  7. לוחצים על שמירה.

התחלת תהליך האימות

כדי להתחיל את תהליך האימות, תוכלו להשתמש בIntent של מסייע הכניסה לחשבון.

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

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

  1. כדי לפענח את האסימון בשפה שלכם, השתמשו בספריית JWT לפענוח, ובעזרת המפתחות הציבוריים של Google (שזמינים בפורמט JWK או PEM) כדי לאמת את החתימה של האסימון.
  2. צריך לוודא שהמנפיק של האסימון (השדה iss באסימון המפוענח) הוא https://accounts.google.com, ושהקהל (השדה aud באסימון המפוענח) הוא הערך של Client-ID ש-Google מנפיקה ל-Actions שלכם, שמוקצה לפרויקט שלכם במסוף Actions on Google.

דוגמה לאסימון מפוענח:

{
  "sub": 1234567890,        // The unique ID of the user's Google Account
  "iss": "https://accounts.google.com",        // The token's issuer
  "aud": "123-abc.apps.googleusercontent.com", // Client ID assigned to your Actions project
  "iat": 233366400,         // Unix timestamp of the token's creation time
  "exp": 233370000,         // Unix timestamp of the token's expiration time
  "name": "Jan Jansen",
  "given_name": "Jan",
  "family_name": "Jansen",
  "email": "jan@gmail.com", // If present, the user's email address
  "locale": "en_US"
}

אם אתם משתמשים בספריית הלקוח של Actions on Google ל-Node.js או בספריית הלקוח של Java, היא מטפלת באימות ובפענוח של האסימון עבורכם, ומעניקה לכם גישה לתוכן הפרופיל, כפי שמוצג בקטעי הקוד הבאים. הערה: בקובץ ה-JSON שמוצג בהמשך מתוארת בקשת webhook ל-Dialogflow ול-Actions SDK, בהתאמה.

קטעי הקוד הבאים משתמשים ב-Dialogflow כדי להיכנס:

Node.js
const {dialogflow, SignIn} = require('actions-on-google');
const app = dialogflow({
  // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
  clientId: CLIENT_ID,
});

// Intent that starts the account linking flow.
app.intent('Start Signin', (conv) => {
  conv.ask(new SignIn('To get your account details'));
});
// Create a Dialogflow intent with the `actions_intent_SIGN_IN` event.
app.intent('Get Signin', (conv, params, signin) => {
  if (signin.status === 'OK') {
    const payload = conv.user.profile.payload;
    conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
  } else {
    conv.ask(`I won't be able to save your data, but what do you want to do next?`);
  }
});
Java
private String clientId = "<your_client_id>";

@ForIntent("Start Signin")
public ActionResponse text(ActionRequest request) {
  ResponseBuilder rb = getResponseBuilder(request);
  return rb.add(new SignIn().setContext("To get your account details")).build();
}
@ForIntent("actions.intent.SIGN_IN")
public ActionResponse getSignInStatus(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    responseBuilder.add(
        "I got your account details, "
            + profile.get("given_name")
            + ". What do you want to do next?");
  } else {
    responseBuilder.add("I won't be able to save your data, but what do you want to do next?");
  }
  return responseBuilder.build();
}

private GoogleIdToken.Payload getUserProfile(String idToken) {
  GoogleIdToken.Payload profile = null;
  try {
    profile = decodeIdToken(idToken);
  } catch (Exception e) {
    LOGGER.error("error decoding idtoken");
    LOGGER.error(e.toString());
  }
  return profile;
}

private GoogleIdToken.Payload decodeIdToken(String idTokenString)
    throws GeneralSecurityException, IOException {
  HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
  JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance();
  GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
          // Specify the CLIENT_ID of the app that accesses the backend:
          .setAudience(Collections.singletonList(clientId))
          .build();
  GoogleIdToken idToken = verifier.verify(idTokenString);
  return idToken.getPayload();
}
Dialogflow JSON
{
  "responseId": "",
  "queryResult": {
    "queryText": "",
    "action": "",
    "parameters": {},
    "allRequiredParamsPresent": true,
    "fulfillmentText": "",
    "fulfillmentMessages": [],
    "outputContexts": [],
    "intent": {
      "name": "Get Signin",
      "displayName": "Get Signin"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {},
    "languageCode": ""
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "isInSandbox": true,
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          },
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.WEB_BROWSER"
          }
        ]
      },
      "inputs": [
        {
          "rawInputs": [],
          "intent": "",
          "arguments": [
            {
              "name": "SIGN_IN",
              "extension": {
                "@type": "type.googleapis.com/google.actions.v2.SignInValue",
                "status": "OK"
              }
            }
          ]
        }
      ],
      "user": {
        "idToken": "peJaCGci..."
      },
      "conversation": {},
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ]
    }
  },
  "session": ""
}

קטעי הקוד הבאים משתמשים ב-Actions SDK לצורך כניסה:

Node.js
const {actionssdk, SignIn} = require('actions-on-google');
const app = actionssdk({
  // REPLACE THE PLACEHOLDER WITH THE CLIENT_ID OF YOUR ACTIONS PROJECT
  clientId: CLIENT_ID,
});

// Intent that starts the account linking flow.
app.intent('actions.intent.TEXT', (conv) => {
  conv.ask(new SignIn('To get your account details'));
});
// Create an Actions SDK intent with the `actions_intent_SIGN_IN` event.
app.intent('actions.intent.SIGN_IN', (conv, params, signin) => {
  if (signin.status === 'OK') {
    const payload = conv.user.profile.payload;
    conv.ask(`I got your account details, ${payload.name}. What do you want to do next?`);
  } else {
    conv.ask(`I won't be able to save your data, but what do you want to do next?`);
  }
});
Java
private String clientId = "<your_client_id>";

@ForIntent("actions.intent.TEXT")
public ActionResponse text(ActionRequest request) {
  ResponseBuilder rb = getResponseBuilder(request);
  return rb.add(new SignIn().setContext("To get your account details")).build();
}
@ForIntent("actions.intent.SIGN_IN")
public ActionResponse getSignInStatus(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    responseBuilder.add(
        "I got your account details, "
            + profile.get("given_name")
            + ". What do you want to do next?");
  } else {
    responseBuilder.add("I won't be able to save your data, but what do you want to do next?");
  }
  return responseBuilder.build();
}

private GoogleIdToken.Payload getUserProfile(String idToken) {
  GoogleIdToken.Payload profile = null;
  try {
    profile = decodeIdToken(idToken);
  } catch (Exception e) {
    LOGGER.error("error decoding idtoken");
    LOGGER.error(e.toString());
  }
  return profile;
}

private GoogleIdToken.Payload decodeIdToken(String idTokenString)
    throws GeneralSecurityException, IOException {
  HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
  JacksonFactory jsonFactory = JacksonFactory.getDefaultInstance();
  GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
          // Specify the CLIENT_ID of the app that accesses the backend:
          .setAudience(Collections.singletonList(this.clientId))
          .build();
  GoogleIdToken idToken = verifier.verify(idTokenString);
  return idToken.getPayload();
}
Actions SDK JSON
{
  "user": {
    "idToken": "peJaCGci..."
  },
  "device": {},
  "surface": {
    "capabilities": [
      {
        "name": "actions.capability.SCREEN_OUTPUT"
      },
      {
        "name": "actions.capability.AUDIO_OUTPUT"
      },
      {
        "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
      },
      {
        "name": "actions.capability.WEB_BROWSER"
      }
    ]
  },
  "conversation": {},
  "inputs": [
    {
      "rawInputs": [],
      "intent": "actions.intent.SIGN_IN",
      "arguments": [
        {
          "name": "SIGN_IN",
          "extension": {
            "@type": "type.googleapis.com/google.actions.v2.SignInValue",
            "status": "OK"
          }
        }
      ]
    }
  ],
  "availableSurfaces": [
    {
      "capabilities": [
        {
          "name": "actions.capability.SCREEN_OUTPUT"
        },
        {
          "name": "actions.capability.AUDIO_OUTPUT"
        },
        {
          "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
        },
        {
          "name": "actions.capability.WEB_BROWSER"
        }
      ]
    }
  ]
}

טיפול בבקשות גישה לנתונים

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

Node.js
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
const auth = admin.auth();
const db = admin.firestore();

// Save the user in the Firestore DB after successful signin
app.intent('Get Sign In', async (conv, params, signin) => {
  if (signin.status !== 'OK') {
    return conv.close(`Let's try again next time.`);
  }
  const color = conv.data[Fields.COLOR];
  const {email} = conv.user;
  if (!conv.data.uid && email) {
    try {
      conv.data.uid = (await auth.getUserByEmail(email)).uid;
    } catch (e) {
      if (e.code !== 'auth/user-not-found') {
        throw e;
      }
      // If the user is not found, create a new Firebase auth user
      // using the email obtained from the Google Assistant
      conv.data.uid = (await auth.createUser({email})).uid;
    }
  }
  if (conv.data.uid) {
    conv.user.ref = db.collection('users').doc(conv.data.uid);
  }
  conv.close(`I saved ${color} as your favorite color for next time.`);
});

// Retrieve the user's favorite color if an account exists, ask if it doesn't.
app.intent('Default Welcome Intent', async (conv) => {
  const {payload} = conv.user.profile;
  const name = payload ? ` ${payload.given_name}` : '';
  conv.ask(`Hi${name}!`);
  // conv.user.ref contains the id of the record for the user in a Firestore DB
  if (conv.user.ref) {
    const doc = await conv.user.ref.get();
    if (doc.exists) {
      const color = doc.data()[Fields.COLOR];
      return conv.ask(`Your favorite color was ${color}. ` +
        'Tell me a color to update it.');
    }
  }
  conv.ask(`What's your favorite color?`);
});
Java
private class FirestoreManager {
  private final Firestore db;
  private final DocumentReference userDocRef;
  private final String uid;
  public FirestoreManager(String databaseUrl, String email)
      throws IOException, FirebaseAuthException {
    if (FirebaseApp.getApps().isEmpty()) {
      // Use the application default credentials (works on GCP based hosting).
      FirebaseOptions options =
          new FirebaseOptions.Builder()
              .setCredentials(GoogleCredentials.getApplicationDefault())
              .setDatabaseUrl(databaseUrl)
              .build();
      FirebaseApp.initializeApp(options);
    }
    this.db = FirestoreClient.getFirestore();
    UserRecord userRecord;
    try {
      userRecord = FirebaseAuth.getInstance().getUserByEmail(email);
    } catch (FirebaseAuthException e) {
      if (e.getErrorCode() == FIREBASE_USER_NOT_FOUND_ERROR) {
        UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest().setEmail(email);
        userRecord = FirebaseAuth.getInstance().createUser(createRequest);
      } else {
        throw e;
      }
    }
    uid = userRecord.getUid();
    userDocRef = db.collection(FIRESTORE_USERS_PATH).document(uid);
  }

  public String readUserColor() throws ExecutionException, InterruptedException {
    ApiFuture<DocumentSnapshot> future = userDocRef.get();
    // future.get() blocks on response
    DocumentSnapshot document = future.get();
    if (document.exists()) {
      return document.get(COLOR_KEY).toString();
    } else {
      return "";
    }
  }
  public Timestamp writeUserColor(String color) throws ExecutionException, InterruptedException {
    Map<String, Object> docData = new HashMap<>();
    docData.put(COLOR_KEY, color);
    ApiFuture<WriteResult> future = userDocRef.set(docData);
    // future.get() blocks on response
    return future.get().getUpdateTime();
  }
}

@ForIntent("Get Sign In")
public ActionResponse getSignIn(ActionRequest request) {
  LOGGER.info("Get sign in intent start.");
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.isSignInGranted()) {
    String color = request.getConversationData().get(COLOR_KEY).toString();
    GoogleIdToken.Payload profile = getUserProfile(request.getUser().getIdToken());
    try {
      FirestoreManager firestoreManager =
          new FirestoreManager(DATABASE_URL, profile.getEmail());
      saveColor(firestoreManager, color);
    } catch (Exception e) {
      LOGGER.error(e.toString());
    }
    responseBuilder
        .add("I saved " + color + " as your favorite color for next time.")
        .endConversation();
  } else {
    responseBuilder.add("Let's try again next time");
  }
  LOGGER.info("Get sign in intent end.");
  return responseBuilder.build();
}

private void saveColor(FirestoreManager firestoreManager, String color) {
  try {
    Timestamp updateTime = firestoreManager.writeUserColor(color);
    LOGGER.info(String.format("Update time: %s", updateTime.toString()));
  } catch (Exception e) {
    LOGGER.error(e.toString());
  }
}