Kontoverknüpfung mit Google Log-in (Dialogflow)

Google Log-in für Assistant bietet Nutzern und Entwicklern die einfachste und einfachste Möglichkeit, Konten zu verknüpfen und zu erstellen. Deine Aktion kann während einer Unterhaltung Zugriff auf das Google-Profil des Nutzers anfordern, einschließlich des Namens, der E-Mail-Adresse und des Profilbilds des Nutzers.

Die Profilinformationen können verwendet werden, um eine personalisierte User Experience in deiner Aktion zu erstellen. Wenn Sie Anwendungen auf anderen Plattformen haben und diese Google Log-in verwenden, können Sie auch ein vorhandenes Nutzerkonto finden und verknüpfen, ein neues Konto erstellen und einen direkten Kommunikationskanal für den Nutzer einrichten.

Für eine Kontoverknüpfung mit Google Log-in musst du den Nutzer um seine Einwilligung für den Zugriff auf sein Google-Profil bitten. Anschließend verwenden Sie die Informationen in seinem Profil, z. B. seine E-Mail-Adresse, um den Nutzer in Ihrem System zu identifizieren.

Google Log-in-Kontoverknüpfung implementieren

Führe die Schritte in den folgenden Abschnitten aus, um eine Google Log-in-Kontoverknüpfung zu deiner Aktion hinzuzufügen.

Projekt konfigurieren

So konfigurieren Sie Ihr Projekt für die Verwendung der Google Log-in-Kontoverknüpfung:

  1. Öffnen Sie die Actions Console und wählen Sie ein Projekt aus.
  2. Klicken Sie auf den Tab Entwickeln und wählen Sie Kontoverknüpfung aus.
  3. Aktivieren Sie den Schalter neben Kontoverknüpfung.
  4. Wählen Sie im Abschnitt Kontoerstellung die Option Ja aus.
  5. Wählen Sie unter Art der Verknüpfung die Option Google Log-in aus.

  6. Öffnen Sie Client-Informationen und notieren Sie sich den Wert der von Google für Ihre Aktionen ausgegebenen Client-ID.

  7. Klicken Sie auf Speichern.

Authentifizierungsvorgang starten

Verwenden Sie den Hilfs-Intent für die Kontoanmeldung, um den Authentifizierungsvorgang zu starten.

Nachdem der Nutzer Ihre Aktion für den Zugriff auf sein Google-Profil autorisiert hat, erhalten Sie ein Google-ID-Token, das in jeder nachfolgenden Anfrage an Ihre Aktion die Google-Profilinformationen des Nutzers enthält.

Damit Sie auf die Profilinformationen des Nutzers zugreifen können, müssen Sie zuerst das Token validieren und decodieren. Gehen Sie dazu so vor:

  1. Verwenden Sie zum Decodieren des Tokens eine JWT-Decodierungsbibliothek für Ihre Sprache und verwenden Sie die öffentlichen Schlüssel von Google (verfügbar im Format JWK oder PEM), um die Tokensignatur zu prüfen.
  2. Prüfen Sie, ob der Aussteller des Tokens (Feld iss im decodierten Token) https://accounts.google.com ist und ob die Zielgruppe (Feld aud im decodierten Token) dem Wert der von Google für Ihre Aktionen ausgegebenen Client-ID entspricht. Diese ist Ihrem Projekt in der Actions on Google-Konsole zugewiesen.

Im Folgenden finden Sie ein Beispiel für ein decodiertes Token:

{
  "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"
}

Wenn Sie die Actions on Google-Clientbibliothek für Node.js oder die Java-Clientbibliothek verwenden, übernimmt das Tool die Validierung und Decodierung des Tokens und gibt Ihnen Zugriff auf den Profilinhalt, wie in den folgenden Code-Snippets gezeigt. Im folgenden JSON-Code wird eine Webhook-Anfrage für Dialogflow bzw. Actions SDK beschrieben.

Die folgenden Snippets verwenden Dialogflow für die Anmeldung:

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": ""
}

In den folgenden Snippets wird das Actions SDK für die Anmeldung verwendet:

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"
        }
      ]
    }
  ]
}

Datenzugriffsanfragen verarbeiten

Wenn Sie Datenzugriffsanfragen bearbeiten möchten, müssen Sie nur prüfen, ob der durch das Google-ID-Token geltend gemachte Nutzer bereits in Ihrer Datenbank vorhanden ist. Das folgende Code-Snippet zeigt ein Beispiel dafür, wie geprüft werden kann, ob ein Nutzerkonto bereits in einer Firestore-Datenbank vorhanden ist.

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());
  }
}