การลิงก์บัญชีกับ Google Sign-In (Dialogflow)

Google Sign-In สำหรับ Assistant มอบประสบการณ์ของผู้ใช้ที่ง่ายและสะดวกที่สุด ทั้งผู้ใช้และนักพัฒนาซอฟต์แวร์สำหรับการลิงก์บัญชีและการสร้างบัญชี การดำเนินการของคุณ สามารถขอสิทธิ์เข้าถึงโปรไฟล์ Google ของผู้ใช้ในระหว่างการสนทนา รวมถึงชื่อ อีเมล และรูปโปรไฟล์ของผู้ใช้

สามารถใช้ข้อมูลโปรไฟล์เพื่อสร้างประสบการณ์ที่ปรับเปลี่ยนในแบบของผู้ใช้ได้ ในการดําเนินการของคุณ หากคุณมีแอปในแพลตฟอร์มอื่นๆ ที่แอปเหล่านั้นใช้ Google Sign-In คุณยังสามารถค้นหาและลิงก์กับบัญชีของผู้ใช้ที่มีอยู่ สร้างบัญชีใหม่ และสร้างช่องทางการสื่อสารโดยตรงกับผู้ใช้

คุณต้องขอให้ผู้ใช้ให้ความยินยอมเพื่อทำการลิงก์บัญชีกับ Google Sign-In เข้าถึงโปรไฟล์ Google ของตนเอง จากนั้นใช้ข้อมูลในโปรไฟล์ของผู้ใช้เพื่อ เช่น ที่อยู่อีเมล เพื่อระบุผู้ใช้ในระบบของคุณ

ใช้การลิงก์บัญชี Google Sign-In

ทําตามขั้นตอนในส่วนต่อไปนี้เพื่อเพิ่มการลิงก์บัญชี Google Sign-In กับ แอ็กชัน

กำหนดค่าโปรเจ็กต์

หากต้องการกำหนดค่าโปรเจ็กต์ให้ใช้การลิงก์บัญชี Google Sign-In ให้ทำตามขั้นตอนต่อไปนี้

  1. เปิดคอนโซล Actions และเลือกโปรเจ็กต์
  2. คลิกแท็บพัฒนา แล้วเลือกการลิงก์บัญชี
  3. เปิดใช้สวิตช์ข้างการลิงก์บัญชี
  4. ในส่วนการสร้างบัญชี ให้เลือกใช่
  5. ในส่วนประเภทการลิงก์ ให้เลือก Google Sign In

  6. เปิดข้อมูลลูกค้า และจดบันทึกค่าของรหัสไคลเอ็นต์ที่ Google ออกให้กับการดำเนินการของคุณ

  7. คลิกบันทึก

เริ่มขั้นตอนการตรวจสอบสิทธิ์

ใช้จุดประสงค์ของตัวช่วยในการลงชื่อเข้าใช้บัญชี เพื่อเริ่มขั้นตอนการตรวจสอบสิทธิ์

หลังจากผู้ใช้ให้สิทธิ์การทำงานของคุณในการเข้าถึงโปรไฟล์ Google ของตน คุณจะได้รับ โทเค็นรหัส Google ที่มีข้อมูลโปรไฟล์ Google ของผู้ใช้ในทุกๆ หน้า คำขอให้ดำเนินการของคุณ

หากต้องการเข้าถึงข้อมูลโปรไฟล์ของผู้ใช้ คุณจะต้องตรวจสอบและถอดรหัสโทเค็นก่อน โดยทำดังนี้

  1. ใช้ไลบรารีการถอดรหัส JWT สำหรับภาษาของคุณเพื่อถอดรหัส โทเค็น และใช้คีย์สาธารณะของ Google (พร้อมใช้งานใน JWK หรือรูปแบบ PEM) เพื่อยืนยันลายเซ็นของโทเค็น
  2. ยืนยันว่าผู้ออกโทเค็น (ช่อง iss ในโทเค็นที่ถอดรหัสแล้ว) คือ https://accounts.google.com และกลุ่มเป้าหมาย (ช่อง aud ในโทเค็นถอดรหัส) เป็นค่าของ รหัสไคลเอ็นต์ที่ Google ออกให้กับการดำเนินการของคุณ ซึ่งกำหนดให้กับโปรเจ็กต์ของคุณ ในคอนโซล 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 ด้านล่างอธิบายถึงคำขอเว็บฮุคสำหรับ 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();
}
Action 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());
  }
}